Skip to content

Apple SwiftData Integration (iOS 17+ & macOS 14+)

This guide shows how to integrate AirDB with SwiftData, Apple's modern persistence framework using @Model and ModelContainer.


1. Defining the @Model Changelog Entity

import Foundation
import SwiftData

@Model
public final class AirDbChangeEntity {
    @Attribute(.unique)
    public var compositeKey: String // collection + ":" + id

    public var collection: String
    public var recordId: String
    public var mutationType: Int
    public var payload: Data

    #Index([\.hlcTimestamp])
    public var hlcTimestamp: String

    public var authorPublicKey: Data?
    public var signature: Data?
    public var createdAt: Date

    public init(
        collection: String,
        recordId: String,
        mutationType: Int,
        payload: Data,
        hlcTimestamp: String,
        authorPublicKey: Data? = nil,
        signature: Data? = nil,
        createdAt: Date = Date()
    ) {
        self.compositeKey = "\(collection):\(recordId)"
        self.collection = collection
        self.recordId = recordId
        self.mutationType = mutationType
        self.payload = payload
        self.hlcTimestamp = hlcTimestamp
        self.authorPublicKey = authorPublicKey
        self.signature = signature
        self.createdAt = createdAt
    }
}

2. Implementing AirDbAdapter

import AirDbKit
import Foundation
import SwiftData

public final class SwiftDataAirDbAdapter: AirDbAdapter, @unchecked Sendable {
    private let container: ModelContainer

    public init(container: ModelContainer) {
        self.container = container
    }

    @MainActor
    public func getChangesSince(sinceHlcTimestamp: String?, limit: Int) async throws -> [ChangeRecord] {
        let context = container.mainContext

        var descriptor: FetchDescriptor<AirDbChangeEntity>
        if let since = sinceHlcTimestamp {
            descriptor = FetchDescriptor<AirDbChangeEntity>(
                predicate: #Predicate { $0.hlcTimestamp > since },
                sortBy: [SortDescriptor(\.hlcTimestamp, order: .forward)]
            )
        } else {
            descriptor = FetchDescriptor<AirDbChangeEntity>(
                sortBy: [SortDescriptor(\.hlcTimestamp, order: .forward)]
            )
        }
        descriptor.fetchLimit = limit

        let entities = try context.fetch(descriptor)
        return entities.compactMap { e in
            guard let type = MutationType(rawValue: e.mutationType) else { return nil }
            return ChangeRecord(
                id: e.recordId,
                collection: e.collection,
                type: type,
                payload: e.payload,
                hlcTimestamp: e.hlcTimestamp,
                authorPublicKey: e.authorPublicKey,
                signature: e.signature
            )
        }
    }

    @MainActor
    public func applyChanges(_ changes: [ChangeRecord]) async throws {
        let context = container.mainContext

        for change in changes {
            let key = "\(change.collection):\(change.id)"
            var fetchDesc = FetchDescriptor<AirDbChangeEntity>(predicate: #Predicate { $0.compositeKey == key })
            fetchDesc.fetchLimit = 1

            let existing = try context.fetch(fetchDesc).first
            if let existing = existing, HlcComparator.compare(change.hlcTimestamp, existing.hlcTimestamp) <= 0 {
                continue
            }

            if let existing = existing {
                existing.mutationType = change.type.rawValue
                existing.payload = change.payload
                existing.hlcTimestamp = change.hlcTimestamp
                existing.authorPublicKey = change.authorPublicKey
                existing.signature = change.signature
            } else {
                let entity = AirDbChangeEntity(
                    collection: change.collection,
                    recordId: change.id,
                    mutationType: change.type.rawValue,
                    payload: change.payload,
                    hlcTimestamp: change.hlcTimestamp,
                    authorPublicKey: change.authorPublicKey,
                    signature: change.signature
                )
                context.insert(entity)
            }
        }

        try context.save()
    }

    @MainActor
    public func getLatestHlc() async throws -> String? {
        let context = container.mainContext
        var desc = FetchDescriptor<AirDbChangeEntity>(
            sortBy: [SortDescriptor(\.hlcTimestamp, order: .reverse)]
        )
        desc.fetchLimit = 1
        return try context.fetch(desc).first?.hlcTimestamp
    }
}