Documentation Index
Fetch the complete documentation index at: https://docs.ditto.live/llms.txt
Use this file to discover all available pages before exploring further.
Overview
This guide covers the essential changes needed to migrate your Ditto Swift app from v4 to v5. The main architectural shift is moving from identity-based initialization to a four-phase configuration model with async/await APIs. Also required: v5 uses DQL (Ditto Query Language) for all data operations. See the DQL Migration Guide for query migration steps.AI Agent Prompt
Use this prompt when working with an AI coding assistant to migrate your Ditto Swift app from v4 to v5.Copy AI Migration Prompt (Click to Expand)
Copy AI Migration Prompt (Click to Expand)
DittoObjC Removal
In v4 when adding theDittoSwift package, it would add in the DittoObjC package product dependencies. In v5, DittoObjC is no longer a separate product β itβs bundled into DittoSwift. This means that you no longer need to add the DittoObjC package product dependencies to your project and you need to remove the DittoObjC package product dependencies from your project in order to get the project to build.
These are the following places the DittoObjC package product dependencies are used:
PBXBuildFilesection β removed theDittoObjC in FrameworksentryPBXFrameworksBuildPhaseβ removedDittoObjC in Frameworksfrom thefilesarrayPBXNativeTarget.packageProductDependenciesβ removed theDittoObjCreferenceXCSwiftPackageProductDependencyβ removed the entireDittoObjCdependency entry
Ditto Instance Initialization
v5 separates initialization into four distinct phases for better clarity and control:- Clear separation of connection, initialization, and authentication concerns
- Async initialization prevents blocking the main thread
- No workarounds needed (transport config, DQL strict mode, v3 sync disable)
- Type-safe configuration with compile-time validation
- Ditto.open() returns a non-optional Ditto instance
Replace Configuration and Initialization
Replace Key changes:
Ditto(identity:) constructor with Ditto.open(config:).- Use
DittoConfiginstead ofDittoIdentity appIDβdatabaseID.onlinePlaygroundβ.server(url:).offlinePlaygroundβ.smallPeersOnly(privateKey: nil)- Add
try awaitfor async initialization - Remove
updateTransportConfig,disableSyncWithV3(), and DQL strict mode queries
Update Authentication
Set authentication handler separately after initialization.Key changes:
- Authentication now uses
expirationHandlerclosure - The handler signature is
@Sendable (_ ditto: Ditto, _ timeUntilExpiration: TimeInterval) async -> Void - Handler called when auth required (
timeUntilExpiration == 0) or token expiring - Use
.developmentprovider for playground tokens - Custom auth: extend
DittoAuthenticationProviderwith a static property, or useDittoAuthenticationProvider("your-provider")
Auth Handler: Swift 6 Actor Isolation Requirement
If yourDittoManager (or whatever class holds the Ditto instance) is an actor,
the expirationHandler closure is @Sendable and executes off the actor. Any access
to actor-isolated state inside a Task { } block requires await:
Additional Changes
DQL Strict Mode Behavior Change
Choose the appropriate migration path based on your current v4 configuration:Currently Using DQL with DQL_STRICT_MODE=true (v4 default)
Currently Using DQL with DQL_STRICT_MODE=true (v4 default)
If youβre currently using the v4 default (
DQL_STRICT_MODE=true), you must explicitly set strict mode to true in v5 before starting sync or executing any queries.To migrate to
DQL_STRICT_MODE=false (the new v5 default), contact Ditto Customer Support for guidance.Currently Using DQL with DQL_STRICT_MODE=false
Currently Using DQL with DQL_STRICT_MODE=false
If you explicitly set
DQL_STRICT_MODE=false in v4, no changes are required.v5 uses DQL_STRICT_MODE=false as the default, so your existing DQL queries will behave identically. You can upgrade freely.Optional: You can remove the explicit ALTER SYSTEM SET DQL_STRICT_MODE = false statement in v5 since this is now the default behavior.Currently Using Legacy Query Builder
Currently Using Legacy Query Builder
The Legacy Query Builder has been removed in v5. All queries must be converted to DQL before upgrading.Good news: Legacy Query Builder functionality has 1:1 support with
DQL_STRICT_MODE=false (the v5 default), making migrations straightforward.Migration Steps:-
In v4: Set
DQL_STRICT_MODE=falseafter initialization: - Convert all queries from Legacy Query Builder to DQL See the Swift LegacyβDQL Migration Guide for detailed conversion examples.
-
Upgrade to v5
No DQL configuration changes requiredβv5 defaults to
DQL_STRICT_MODE=false.
Default Persistence Directory
v5 includes the database ID in the default directory name:ditto-{databaseID} instead of ditto. This is for only new databases created in v5. v5 does not migrate existing databases to the new structure.
Provide custom persistence directory:
Observer Changes
v4 and v5 both use callback-based observers. The coreregisterObserver API pattern is the same in both versions, returning a DittoStoreObserver that you retain and call .stop() on for cleanup.
- v5 adds
Codableargument overloads forregisterObserver - Call
item.dematerialize()after extracting data fromDittoQueryResultItemto free native memory - The observer lifecycle pattern (retain,
.stop(), cleanup) is the same as v4
Back Pressure (Signal Next)
The standardregisterObserver automatically signals readiness for the next callback after your handler returns. For handlers that need time to process results β such as expensive rendering or batch operations β use registerObserver(query:arguments:deliverOn:handlerWithSignalNext:) to control when the next callback is delivered.
Coalescing behavior: While your handler is processing, Ditto coalesces any
intermediate changes. When you call
signalNext(), you receive a single
callback with the latest state β not every intermediate change.- Processing updates is computationally expensive
- Youβre performing async work (network calls, heavy rendering) on each update
- Query results change very frequently and you want to process only the latest state
DittoDiskUsageItem Property Names (v5, Unchanged)
DiskUsageItem was renamed to DittoDiskUsageItem, however, the property names
are unchanged in v5:
.path: Stringβ unchanged.sizeInBytes: Intβ unchanged.childItems: [DittoDiskUsageItem]β unchanged (type renamed, property name same)
DittoConnection Property Renames
| v4.14 | v5.0 |
|---|---|
connection.peerKeyString1 | connection.peer1 |
connection.peerKeyString2 | connection.peer2 |
connection.approximateDistanceInMeters | REMOVED β no replacement |
DittoPeer Property Renames (v5)
| v4.14 | v5.0 |
|---|---|
peer.peerKeyString | peer.peerKey |
peer.osV2: DittoPeerOS? | peer.os: DittoPeerOS? |
peer.isConnectedToDittoCloud | peer.isConnectedToDittoServer |
Breaking: DittoConnection Conformances Cause Swift 6 filter Ambiguity
In v5, DittoConnection gains Identifiable, Equatable, Hashable, and Sendable conformances.
In Swift 6, Foundation.filter(_:Predicate) takes precedence over Sequence.filter(_:) when the
element type conforms to these protocols, causing an unexpected compile error.
Error: trailing closure passed to parameter of type 'Predicate<DittoConnection>'
Fix: Annotate the closure parameter type explicitly:
DittoLogger Changes
DittoLogger retains the same class name in both v4 and v5. The key changes are:
- v5 marks
DittoLoggerasfinalandSendablefor Swift concurrency safety DittoLogger.setLogFile()is removed in v5 (was deprecated in v4 β useexport(to:)instead)
API Changes
| v4.14 | v5.0 |
|---|---|
ditto.startSync() | ditto.sync.start() |
ditto.stopSync() | ditto.sync.stop() |
ditto.isSyncActive | ditto.sync.isActive |
ditto.observePeers(_:) / observePeersV2(_:) | ditto.presence.observe(didChangeHandler:) |
ditto.activated | ditto.isActivated |
peerKeyString | peerKey |
isConnectedToDittoCloud | isConnectedToDittoServer |
Ditto(identity:) | Ditto.open(config:) (async) or Ditto.openSync(config:) |
ditto.siteID | Not a standalone property β access via ditto.presence.graph.localPeer |
ditto.appID | ditto.config.databaseID |
DiskUsage class | DittoDiskUsage class |
ditto.diskUsage (typed DiskUsage) | ditto.diskUsage (now typed DittoDiskUsage) |
DiskUsage.exec: DiskUsageItem | DittoDiskUsage.item: DittoDiskUsageItem |
DiskUsage.DiskUsageObserverHandle (nested) | DittoDiskUsageObserver (standalone class) |
DiskUsage.observe(eventHandler:) returns DiskUsageObserverHandle | DittoDiskUsage.observe(eventHandler:) returns DittoDiskUsageObserver |
DiskUsageItem | DittoDiskUsageItem |
DittoSwiftError typealias | Removed β use DittoError directly |
DittoError.migrationError(reason:) | Removed |
MigrationErrorReason enum | Removed |
DittoSmallPeerInfo.syncScope: DittoSmallPeerInfoSyncScope | DQL: ALTER SYSTEM SET sync_scope = '...' |
DittoSmallPeerInfoSyncScope enum | Removed β configure via DQL |
DittoSmallPeerInfo.metadataJSONString | DittoSmallPeerInfo.metadataJSONData |
DittoSmallPeerInfo.setMetadataJSONString(_:) | DittoSmallPeerInfo.setMetadataJSONData(_:) |
DittoLogger.setLogFile() | Removed β use export(to:) instead |
WebSocket Sync Default Changed
Sync Subscription Changes
DittoSync.subscriptions is now computed rather than a stored Swift Set. Do not cache this collection β query it fresh when needed. The same applies to DittoStore.observers.
APIs Removed
These v4 APIs were removed| Removed API | Notes |
|---|---|
Ditto.transportDiagnostics() / DittoTransportDiagnostics | Low-level transport debug info removed |
Ditto.runGarbageCollection() | GC is now handled automatically |
Ditto.disableSyncWithV3() | v3 protocol support fully dropped |
Ditto.isHistoryTrackingEnabled / history tracking initializer | History tracking removed entirely |
DittoIdentity.manual(certificateConfig:) | Manual certificate identity removed |
DittoConnection.approximateDistanceInMeters | Peer distance estimation removed |
DittoHTTPListenConfig.staticContentPath | Static HTTP content serving removed |
DittoStore.queriesHash(queries:) | Query set hashing removed |
DittoStore.queriesHashMnemonic(queries:) | Query mnemonic hashing removed |
DittoConnectionPriority enum | Connection priority control removed |
DittoTransportSnapshot struct | Removed with transport diagnostics |
DittoRemotePeer struct (deprecated) | Use DittoPeer in DittoPresenceGraph |
DittoAddress struct | Removed |
DittoExperimental.jsonByTranscoding(cbor:) | CBOR-to-JSON utility removed |
DittoLogger.setLogFile() | Use export(to:) instead |
New APIs in v5
Ditto.open(config:): async throws -> Dittoβ async factory methodDitto.openSync(config:): throws -> Dittoβ synchronous factory alternativeditto.config: DittoConfigβ read the config used to open this instanceditto.isActivated: Boolβ replacesditto.activatedDittoStore.newAttachment(data: Data, metadata:)β create attachment from in-memoryData(v4 was file path only)DittoStore.registerObserver(query:arguments: some Codable, ...)β overloads acceptingCodablequery argumentsDittoSync.registerSubscription(query:arguments: some Codable)β overload acceptingCodableargumentsDittoDiskUsageObserverβ standalone observer class (was nestedDiskUsage.DiskUsageObserverHandle)DittoDiskUsageItemβ renamed fromDiskUsageItemDittoSyncSubscription.queryArgumentsCBORData/.queryArgumentsJSONData/.idβ new properties for introspectionDittoConnectionnow conforms toIdentifiable,Equatable,Hashable, andSendable- All public types now
finalandSendablefor Swift concurrency DittoConfigandDittoConfigConnectnow conform toSendableandCodable
Migration Checklist
Initialization
- Replace
Ditto(identity:)withDitto.open(config:) - Create
DittoConfigwithdatabaseIDandconnectmode - Add
try awaitfor async initialization - Update
.onlinePlaygroundβ.server(url:) - Update
.offlinePlaygroundβ.smallPeersOnly(privateKey: nil) - Remove
updateTransportConfigcalls - Remove
disableSyncWithV3()calls - Set
DQL_STRICT_MODE=trueBEFORE starting sync if maintaining v4 behavior - Update
startSync()βsync.start()
Authentication
- Set
expirationHandlerclosure after initialization - Use
.developmentprovider for playground tokens - Remove authentication from identity configuration
Observers
- Call
item.dematerialize()after extracting data fromDittoQueryResultItemto free native memory - If processing is expensive, use
handlerWithSignalNext:for back-pressure control - v5 adds
Codableargument overloads β adopt where convenient
Data Operations
- Replace all
.collection("x").find(...)chains withstore.execute()DQL - Use parameterized queries with
:paramNameβ never string interpolation - Replace
.upsert()withINSERT INTO ... ON ID CONFLICT DO UPDATE - Replace
.update {}closures withUPDATE SETDQL - Replace
.remove()withDELETE FROM - Replace
.evict()withEVICT FROM - Replace
.observeLocal {}withstore.registerObserver()callback - Replace
.subscribe()withsync.registerSubscription() - Replace
counter?.increment(by:)withUPDATE APPLY ... PN_INCREMENT BY
Breaking Changes
- Set
persistenceDirectoryif maintaining v4 directory structure - Update
peerKeyStringβpeerKey - Update
diskUsageusage:DiskUsageβDittoDiskUsage,.execβ.item,DiskUsageObserverHandleβDittoDiskUsageObserver - Update
DittoConnection.peer1/peer2comparisons: nowStringinstead ofData - Update
isConnectedToDittoCloudβisConnectedToDittoServer - Update
ditto.observePeers(_:)βditto.presence.observe(didChangeHandler:) - Update
ditto.activatedβditto.isActivated - Remove all
DittoSwiftErrorreferences β useDittoErrordirectly - Update
DittoSmallPeerInfo.metadataJSONStringβmetadataJSONData - Check for explicit
webSocketSync = trueif your app relied on v4 default - Update
DittoLogger.setLogFile()calls β useexport(to:)instead
Verification
- Build compiles with zero errors
- No deprecated API warnings
- Observers update UI immediately
- Authentication works before sync starts
- No memory leaks on navigation
- No memory leaks (Instruments: Allocations, Leaks)
Common Pitfalls
-
DQL_STRICT_MODE silent change: Not setting
DQL_STRICT_MODE=truewhen your v4 app used the default. Objects that were replaced whole will now merge at field level β causes unexpected data merging. -
Forgetting
try await:Ditto.open(config:)is async. Not awaiting it causes a compile error. -
Storing
DittoQueryResultItemoutside callback: These hold native memory. Always calldematerialize()and extract your model before the callback returns. -
String interpolation in queries: Never
"SELECT * FROM cars WHERE color = '\(color)'". Always usearguments: ["color": color]. -
Counter initialization: Donβt put
DittoCounter()or0in insert documents. Counters are created on firstPN_INCREMENT. Inserting0creates a REGISTER, not a COUNTER. -
Missing
ON ID CONFLICT: INSERT fails if document_idalready exists without a conflict clause. -
Not checking
timeUntilExpirationin auth handler: Handler is called for both initial auth (0) and token refresh (>0). Handle both cases.