Read this when an endpoint only returns children for one parent, such as /projects/{id}/tasks or /users/{id}/notes.
The core rule is simple:
- parent-scoped sync always uses an explicit
relationship:key path - relationship-scoped reads always use an explicit
relationship:+relationshipID:
SwiftSync does not infer parent scope for you.
Parent sync has two responsibilities:
- Attach child rows to the provided parent.
- Scope diff/delete to only that parent's children.
SwiftSync uses explicit parent relationships for parent-scoped sync.
Reactive reads always use explicit relationship paths:
@SyncQuery(..., relationship: \.relationship, relationshipID: parentID, ...)
When you call:
try await SwiftSync.sync(
payload: payload,
as: Child.self,
in: context,
parent: parentObject,
relationship: \Child.parent
)SwiftSync uses the provided relationship key path directly; no relationship inference is performed.
Parent sync computes deletions scoped to the parent:
toDelete = (rows belonging to this parent scope) - (payload identities)
If scope resolution is wrong, delete can target valid rows from another logical scope.
That is why the API is explicit here instead of "helpfully" guessing.
Models:
@Model final class Project {
@Attribute(.unique) var id: Int
var name: String
@Relationship(inverse: \Task.project) var tasks: [Task]
}
@Model final class Task {
@Attribute(.unique) var id: Int
var title: String
var project: Project?
}Pass relationship: \Task.project when syncing tasks for a project parent scope.
Models:
@Model final class User {
@Attribute(.unique) var id: Int
var name: String
}
@Model final class Ticket {
@Attribute(.unique) var id: Int
var title: String
var assignee: User?
var reviewer: User?
}If parent passed is a User, both assignee and reviewer are valid candidates.
Choose the intended path explicitly at call sites:
try await SwiftSync.sync(
payload: payload,
as: Ticket.self,
in: context,
parent: user,
relationship: \Ticket.assignee
)SwiftSync does not guess parent relationships. Call sites must declare scope explicitly via relationship:.
@SyncQuery(
Task.self,
relationship: \Task.project,
relationshipID: projectID,
in: syncContainer,
sortBy: [SortDescriptor(\Task.id)]
)
var tasks: [Task]Ambiguous example:
@SyncQuery(
Ticket.self,
relationship: \Ticket.assignee,
relationshipID: userID,
in: syncContainer,
sortBy: [SortDescriptor(\Ticket.id)]
)
var assignedTickets: [Ticket]