Using transactions to group changes

Read and write transactions present a context where multiple changes can be made then finally committed to the DB or rolled back. This ensures that either all the changes get persisted, or no change is made to the DB (in the case of a rollback or exception).

// Delete a list and its todos in a transaction
func deleteList(db: PowerSyncDatabase, listId: String) async throws {
    try await db.writeTransaction { tx in
        try await tx.execute(sql: "DELETE FROM lists WHERE id = ?", parameters: [listId])
        try await tx.execute(sql: "DELETE FROM todos WHERE list_id = ?", parameters: [listId])
    }
}

Also see readTransaction.

Subscribe to changes in data

Use watch to watch for changes to the dependent tables of any SQL query.

// Watch for changes to the lists table
func watchLists(_ callback: @escaping (_ lists: [ListContent]) -> Void ) async {
    do {
        for try await lists in try self.db.watch<ListContent>(
            sql: "SELECT * FROM \(LISTS_TABLE)",
            parameters: [],
            mapper: { cursor in
                try ListContent(
                    id: cursor.getString(name: "id"),
                    name: cursor.getString(name: "name"),
                    createdAt: cursor.getString(name: "created_at"),
                    ownerId: cursor.getString(name: "owner_id")
                )
            }
        ) {
            callback(lists)
        }
    } catch {
        print("Error in watch: \(error)")
    }
}

Insert, update, and delete data in the local database

Use execute to run INSERT, UPDATE or DELETE queries.

// Insert a new TODO
func insertTodo(_ todo: NewTodo, _ listId: String) async throws {
    try await db.execute(
        sql: "INSERT INTO \(TODOS_TABLE) (id, created_at, created_by, description, list_id, completed) VALUES (uuid(), datetime(), ?, ?, ?, ?)",
        parameters: [connector.currentUserID, todo.description, listId, todo.isComplete]
    )
}

Send changes in local data to your backend service

Override uploadData to send local updates to your backend service.

class MyConnector: PowerSyncBackendConnector {
    override func uploadData(database: PowerSyncDatabaseProtocol) async throws {
        let batch = try await database.getCrudBatch()
        guard let batch = batch else { return }
        for entry in batch.crud {
            switch entry.op {
            case .put:
                // Send the data to your backend service
                // Replace `_myApi` with your own API client or service
                try await _myApi.put(table: entry.table, data: entry.opData)
            default:
                // TODO: implement the other operations (patch, delete)
                break
            }
        }
        try await batch.complete(writeCheckpoint: nil)
    }
}

Accessing PowerSync connection status information

Use currentStatus and observe changes to listen for status changes to your PowerSync instance.

import Foundation
import SwiftUI
import PowerSync

struct PowerSyncConnectionIndicator: View {
    private let powersync: any PowerSyncDatabaseProtocol
    @State private var connected: Bool = false
    
    init(powersync: any PowerSyncDatabaseProtocol) {
        self.powersync = powersync
    }
    
    var body: some View {
        let iconName = connected ? "wifi" : "wifi.slash"
        let description = connected ? "Online" : "Offline"
        
        Image(systemName: iconName)
            .accessibility(label: Text(description))
            .task {
                self.connected = powersync.currentStatus.connected
                
                for await status in powersync.currentStatus.asFlow() {
                    self.connected = status.connected
                }
           }
    }
}

Wait for the initial sync to complete

Use the hasSynced property and observe status changes to indicate to the user whether the initial sync is in progress.

struct WaitForFirstSync: View {
    private let powersync: any PowerSyncDatabaseProtocol
    @State var didSync: Bool = false

    init(powersync: any PowerSyncDatabaseProtocol) {
        self.powersync = powersync
    }
    
    var body: some View {
        if !didSync {
            ProgressView().task {
                do {
                    try await powersync.waitForFirstSync()
                } catch {
                    // TODO: Handle errors
                }
            }
        }
    }
}

For async use cases, use waitForFirstSync.

Report sync download progress

You can show users a progress bar when data downloads using the downloadProgress property from the SyncStatusData object. downloadProgress.downloadedFraction gives you a value from 0.0 to 1.0 representing the total sync progress. This is especially useful for long-running initial syncs.

Example:

struct SyncProgressIndicator: View {
    private let powersync: any PowerSyncDatabaseProtocol
    private let priority: BucketPriority?
    @State private var status: SyncStatusData? = nil

    init(powersync: any PowerSyncDatabaseProtocol, priority: BucketPriority? = nil) {
        self.powersync = powersync
        self.priority = priority
    }
    
    var body: some View {
        VStack {
            if let totalProgress = status?.downloadProgress {
                let progress = if let priority = self.priority {
                    totalProgress.untilPriority(priority: priority)
                } else {
                    totalProgress
                }
                
                ProgressView(value: progress.fraction)

                if progress.downloadedOperations == progress.totalOperations {
                    Text("Applying server-side changes...")
                } else {
                    Text("Downloaded \(progress.downloadedOperations) out of \(progress.totalOperations)")
                }
            }
        }.task {
            status = powersync.currentStatus
            for await status in powersync.currentStatus.asFlow() {
                self.status = status
            }
        }
    }
}

Also see: