Swift / iOS
Installation
Section titled “Installation”Swift Package Manager
Section titled “Swift Package Manager”Add to Package.swift:
dependencies: [ .package(url: "https://github.com/trainstar/synchro.git", from: "0.1.0")]Then add "Synchro" to your target’s dependencies:
.target( name: "MyApp", dependencies: ["Synchro"])CocoaPods
Section titled “CocoaPods”pod 'Synchro', '~> 0.1.0'Platforms: iOS 16.0+, macOS 13.0+
Dependencies: GRDB.swift 7.0+
Configuration
Section titled “Configuration”let config = SynchroConfig( dbPath: dbURL.path, serverURL: URL(string: "https://api.example.com")!, authProvider: { await getToken() }, clientID: UIDevice.current.identifierForVendor!.uuidString, appVersion: "1.0.0")let client = try SynchroClient(config: config)SynchroConfig Parameters
Section titled “SynchroConfig Parameters”| Parameter | Type | Default | Description |
|---|---|---|---|
dbPath | String | Required | Path to the SQLite database file |
serverURL | URL | Required | Sync server base URL |
authProvider | @Sendable () async throws -> String | Required | Returns a JWT token for authentication |
clientID | String | Required | Unique device identifier |
platform | String | "ios" | Platform name sent during registration |
appVersion | String | Required | Semantic version of the app |
syncInterval | TimeInterval | 30 | Seconds between sync cycles |
pushDebounce | TimeInterval | 0.5 | Seconds after a write before triggering push |
maxRetryAttempts | Int | 5 | Maximum retry count before entering error state |
pullPageSize | Int | 100 | Rows per pull page (capped at 1000) |
pushBatchSize | Int | 100 | Pending changes per push batch (max 1000) |
snapshotPageSize | Int | 100 | Rows per snapshot page (capped at 1000) |
Core Usage
Section titled “Core Usage”Queries
Section titled “Queries”// Fetch multiple rowslet rows = try client.query( "SELECT * FROM tasks WHERE user_id = ?", params: [userId])
// Fetch a single rowlet task = try client.queryOne( "SELECT * FROM tasks WHERE id = ?", params: [id])Writes
Section titled “Writes”let result = try client.execute( "INSERT INTO tasks (id, title, user_id) VALUES (?, ?, ?)", params: [UUID().uuidString, "Review proposal", userId])// result.rowsAffected == 1Batch Execution
Section titled “Batch Execution”let affected = try client.executeBatch([ SQLStatement(sql: "INSERT INTO tasks (id, title, user_id) VALUES (?, ?, ?)", params: [UUID().uuidString, "Write report", userId]), SQLStatement(sql: "INSERT INTO tasks (id, title, user_id) VALUES (?, ?, ?)", params: [UUID().uuidString, "Update docs", userId]),])// affected == 2Transactions
Section titled “Transactions”// Write transaction: multiple operations atomicallytry client.writeTransaction { db in try db.execute( sql: "INSERT INTO tasks (id, title, user_id) VALUES (?, ?, ?)", arguments: [UUID().uuidString, "Review proposal", userId] ) try db.execute( sql: "INSERT INTO comments (id, task_id, body, status) VALUES (?, ?, ?, ?)", arguments: [UUID().uuidString, taskId, "Looks good", "open"] )}
// Read transaction: consistent snapshotlet count = try client.readTransaction { db in try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM tasks") ?? 0}Observation
Section titled “Observation”Change Notification
Section titled “Change Notification”let cancel = client.onChange(tables: ["tasks"]) { print("tasks table changed")}
// Later: stop observingcancel.cancel()Reactive Query
Section titled “Reactive Query”let cancel = client.watch( "SELECT * FROM tasks ORDER BY created_at DESC", tables: ["tasks"]) { rows in self.tasks = rows}The callback fires immediately with the current result, then again whenever the observed tables change. Observation is backed by GRDB’s ValueObservation, which uses SQLite’s sqlite3_update_hook for efficient change detection.
Sync Control
Section titled “Sync Control”// Start sync: register, fetch schema, begin sync looptry await client.start()
// Trigger an immediate sync cycletry await client.syncNow()
// Stop the sync loopclient.stop()
// Close the database (also stops sync)try client.close()Status and Events
Section titled “Status and Events”Sync Status
Section titled “Sync Status”let cancel = client.onStatusChange { status in switch status { case .idle: print("Idle") case .syncing: print("Syncing...") case .error(let retryAt): if let retryAt { print("Error, retrying at \(retryAt)") } else { print("Error, no retry scheduled") } case .stopped: print("Stopped") }}Conflict Events
Section titled “Conflict Events”let cancel = client.onConflict { event in print("Conflict on \(event.table) record \(event.recordID)") print("Client data: \(String(describing: event.clientData))") print("Server data: \(String(describing: event.serverData))")}Snapshot Required
Section titled “Snapshot Required”let cancel = client.onSnapshotRequired { () async -> Bool in // Prompt the user or decide programmatically return await promptUser("Full resync needed. Continue?")}Error Handling
Section titled “Error Handling”All errors are represented by the SynchroError enum:
public enum SynchroError: Error { case notConnected case schemaNotLoaded case tableNotSynced(String) case upgradeRequired(currentVersion: String, minimumVersion: String) case schemaMismatch(serverVersion: Int64, serverHash: String) case snapshotRequired case pushRejected(results: [PushResult]) case networkError(underlying: Error) case serverError(status: Int, message: String) case databaseError(underlying: Error) case invalidResponse(message: String) case alreadyStarted case notStarted}| Case | When It Occurs |
|---|---|
notConnected | Operation attempted before start() completes registration |
schemaNotLoaded | Schema has not been fetched from server yet |
tableNotSynced | Write attempted on a table not in the server schema |
upgradeRequired | Server rejected the client’s app version (HTTP 426) |
schemaMismatch | Client schema hash does not match server (HTTP 409) |
snapshotRequired | Server indicates a full snapshot is needed |
pushRejected | One or more push records were rejected by the server |
networkError | Network connectivity failure |
serverError | Server returned a non-200 HTTP status |
databaseError | SQLite operation failed |
invalidResponse | Server response could not be decoded |
alreadyStarted | start() called when sync is already running |
notStarted | syncNow() called before start() |