SwiftSync
JSON to SwiftData and back. SwiftData Sync.
Install / Use
/learn @3lvis/SwiftSyncREADME

SwiftSync is a sync layer for SwiftData apps.
Define your models once, read from local SwiftData, and let SwiftSync handle the repetitive sync and export work in between.
Features
- Convention-first JSON -> SwiftData mapping
- Deterministic diffing for inserts, updates, and deletes
- Automatic relationship syncing for nested objects and foreign keys
- Export back into API-ready JSON
- Reactive local reads for SwiftUI and UIKit
Quick Start
One-to-many

import SwiftData
import SwiftSync
@Syncable
@Model
final class User {
@Attribute(.unique) var id: Int
var email: String?
var createdAt: Date?
var updatedAt: Date?
var notes: [Note]
init(id: Int, email: String? = nil, createdAt: Date? = nil, updatedAt: Date? = nil, notes: [Note] = []) {
self.id = id
self.email = email
self.createdAt = createdAt
self.updatedAt = updatedAt
self.notes = notes
}
}
@Syncable
@Model
final class Note {
@Attribute(.unique) var id: Int
var text: String
var user: User?
init(id: Int, text: String, user: User? = nil) {
self.id = id
self.text = text
self.user = user
}
}
JSON
[
{
"id": 6,
"email": "shawn@ovium.com",
"created_at": "2014-02-14T04:30:10+00:00",
"updated_at": "2014-02-17T10:01:12+00:00",
"notes": [
{
"id": 301,
"text": "Call supplier before Friday"
},
{
"id": 302,
"text": "Prepare Q1 budget review"
}
]
}
]
Sync
Create the container:
let syncContainer = try SyncContainer(for: User.self, Note.self)
Then sync the payload:
let payload = try await getUsers()
try await syncContainer.sync(payload: payload, as: User.self)
SwiftUI reacts automatically using @SyncQuery
import SwiftUI
import SwiftSync
struct UsersView: View {
let syncContainer: SyncContainer
@SyncQuery(
User.self,
in: syncContainer,
sortBy: [SortDescriptor(\User.id)]
)
private var users: [User]
var body: some View {
List {
ForEach(users) { user in
Section(user.email ?? "User \\(user.id)") {
ForEach(user.notes) { note in
Text(note.text)
}
}
}
}
}
}
One-to-one

Model
import SwiftData
import SwiftSync
@Syncable
@Model
final class User {
@Attribute(.unique) var id: Int
var name: String
var company: Company?
init(id: Int, name: String, company: Company? = nil) {
self.id = id
self.name = name
self.company = company
}
}
@Syncable
@Model
final class Company {
@Attribute(.unique) var id: Int
var name: String
var user: User?
init(id: Int, name: String, user: User? = nil) {
self.id = id
self.name = name
self.user = user
}
}
JSON
[
{
"id": 6,
"name": "Shawn Merrill",
"company": {
"id": 0,
"name": "Facebook"
}
}
]
Sync
let syncContainer = try SyncContainer(for: User.self, Company.self)
let payload = try await getUsers()
try await syncContainer.sync(payload: payload, as: User.self)
SwiftUI reacts automatically using @SyncQuery
import SwiftUI
import SwiftSync
struct UsersView: View {
let syncContainer: SyncContainer
@SyncQuery(
User.self,
in: syncContainer,
sortBy: [SortDescriptor(\User.id)]
)
private var users: [User]
var body: some View {
List(users) { user in
VStack(alignment: .leading, spacing: 4) {
Text(user.name)
Text(user.company?.name ?? "No company")
.foregroundStyle(.secondary)
}
}
}
}
Install
Add the package in Xcode:
File->Add Package Dependencies...- Use this URL:
https://github.com/3lvis/SwiftSync.git
- Add the
SwiftSynclibrary product to your app target.
If you use Package.swift directly:
.package(url: "https://github.com/3lvis/SwiftSync.git", from: "1.0.0")
Then import:
import SwiftSync
Requirements: Xcode 17+, Swift 6.2, iOS 17+ / macOS 14+
Full Overview
Quick Start already covers the default root-collection path.
The sections below cover the next cases you are likely to hit in a real app:
- One-to-many relationships linked by IDs instead of nested child objects
- Parent-scoped sync for child collections returned under one parent
- Single-item sync for detail payloads and mutation responses
- Relationship payload shapes beyond the simple nested to-many example
- Customization points for mapping, reads, export, and dates
Table of contents:
- One-to-Many w/ child IDs
- Parent-Scoped Sync
- Single-Item Sync
- Property Mapping and Customization
- Reactive Reads
- Exporting JSON
- Date Handling
- Demo App
- Further Reading
- License
One-to-Many w/ child IDs
Use this shape when the parent JSON does not include full child objects and only sends their IDs.
Model
@Syncable
@Model
public final class Project {
@Attribute(.unique) public var id: String
public var name: String
public var tasks: [Task]
}
@Syncable
@Model
public final class Task {
@Attribute(.unique) public var id: String
public var title: String
@NotExport
public var project: Project?
}
JSON
[
{
"id": "C3E7A1B2-1001-0000-0000-000000000001",
"name": "Account Security Controls",
"task_ids": [
"C3E7A1B2-3001-0000-0000-000000000001",
"C3E7A1B2-3001-0000-0000-000000000002"
]
}
]
Sync
try await syncContainer.sync(payload: payload, as: Project.self)
This works when those tasks already exist in SwiftData, or are synced elsewhere. SwiftSync uses the *_ids list to connect the relationship without needing full task objects in the same JSON.
Read
@SyncModel(Project.self, id: projectID, in: syncContainer)
private var project: Project?
This pattern is useful for APIs that return lightweight parent objects and keep the full child records on a separate endpoint.
Parent-Scoped Sync
Use parent-scoped sync when an endpoint returns the children for a single parent, such as /projects/{id}/tasks.
Model
@Syncable
@Model
public final class Task {
@Attribute(.unique) public var id: String
public var title: String
public var projectID: String
@NotExport
public var project: Project?
}
JSON
[
{
"id": "C3E7A1B2-3001-0000-0000-000000000001",
"title": "Add session timeout controls to account settings",
"project_id": "C3E7A1B2-1001-0000-0000-000000000001"
}
]
Sync
try await syncContainer.sync(
payload: payload,
as: Task.self,
parent: project,
relationship: \Task.project
)
The explicit relationship: key path tells SwiftSync which parent these rows belong to. It compares changes only inside that parent’s set of rows, not across the whole table.
Read
let taskPublisher = SyncQueryPublisher(
Task.self,
relationship: \Task.project,
relationshipID: projectID,
in: syncContainer
)
See Parent Scope for the full rules.
Single-Item Sync
Use sync(item:) when an endpoint returns one model at a time, such as a detail screen or the response from saving an edit.
It also works well when that same response includes child data that belongs to that one model.
Model
@Syncable
@Model
public final class Item {
@Attribute(.unique) public var id: String
public var title: String
public var taskID: String
@NotExport
public var task: Task?
}
JSON
{
"id": "C3E7A1B2-3001-0000-0000-000000000001",
"title": "Add session timeout controls to account settings",
"items": [
{
"id": "C3E7A1B2-4001-0000-0000-000000000001",
"task_id": "C3E7A1B2-3001-0000-0000-000000000001",
"title": "Document requirements"
}
]
}
Sync
try await syncContainer.sync(item: payload, as: Task.self)
try await syncContainer.sync(
payload: itemPayload,
as: Item.self,
parent: task,
relationship: \Item.task
)
Read
@SyncModel(Task.self, id: taskID, in: syncContainer)
private var task: Task?
This keeps the detail flow simple:
- Update the task from the single-object response
- Update that task's checklist items from the nested array
- Let both list and detail screens keep reading from the same SwiftData data
Property Mapping
Convention-first mapping is the default. Reach for overrides only when local naming intentionally differs from the backend.
Let's say you have a Task model. The backend sends a top-level description field and a nested state object, but you want the local model to stay straightforward and Swift-friendly:
{
"id": 42,
"title": "Ship README rewrite",
"description": "Tighten up the property mapping docs and examples.",
"state": {
"id": "in_progress",
"label": "In Progress"
}
}
You can keep title convention-based, rename description locally, and flatten the nested state object into plain properties on Task:
@Syncable
@Model
final class Task {
@Attribute(.unique) var id: Int
var title: String
@RemoteKey("description")
var details: String?
@RemoteKey("state.id")
var state: String
@RemoteKey("state.label")
var stateLabel: String
init(
id: Int,
title: String,
details: String? = nil,
state: String,
stateLabel: String
) {
self.id = id
self.title = title
self.details = details
self.state = state
self.stateLabel = stateLabel
}
}
In that example:
- `ti
Related Skills
node-connect
342.0kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
84.7kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
342.0kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
commit-push-pr
84.7kCommit, push, and open a PR
