Skip to main content

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.

Removed Platform Support

Breaking Change: v5 removes support for the following platforms. If your app targets one of these, you cannot upgrade to v5 while shipping on that platform β€” drop the platform target or stay on v4.
  • tvOS β€” No longer supported in v5.0
  • visionOS (beta) β€” No longer supported in v5.0
Check this before starting the rest of the migration. If you have concerns about this change, please reach out to Ditto customer support.

DittoObjC Removal

In v4 when adding the DittoSwift 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:
  1. PBXBuildFile section β€” removed the DittoObjC in Frameworks entry
  2. PBXFrameworksBuildPhase β€” removed DittoObjC in Frameworks from the files array
  3. PBXNativeTarget.packageProductDependencies β€” removed the DittoObjC reference
  4. XCSwiftPackageProductDependency β€” removed the entire DittoObjC dependency entry

Ditto Instance Initialization

v5 separates initialization into four distinct phases for better clarity and control:
These changes provide:
  • 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
Initialization Migration Steps
1

Replace Configuration and Initialization

Replace Ditto(identity:) constructor with Ditto.open(config:).
Key changes:
  • Use DittoConfig instead of DittoIdentity
  • appID β†’ databaseID
  • .onlinePlayground β†’ .server(url:)
  • .offlinePlayground β†’ .smallPeersOnly(privateKey: nil)
  • Add try await for async initialization
  • Remove updateTransportConfig, disableSyncWithV3(), and DQL strict mode queries
2

Update Authentication

Set authentication handler separately after initialization.
Key changes:
  • Authentication now uses expirationHandler closure
  • The handler signature is @Sendable (_ ditto: Ditto, _ timeUntilExpiration: TimeInterval) async -> Void
  • Handler called when auth required (timeUntilExpiration == 0) or token expiring
  • Use .development provider for playground tokens
  • Custom auth: extend DittoAuthenticationProvider with a static property, or use DittoAuthenticationProvider("your-provider")
3

Start Sync

Start sync after initialization.
Key changes:
  • Namespace change from ditto.startSync() to ditto.sync.start()

Auth Handler: Swift 6 Actor Isolation Requirement

If your DittoManager (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

Breaking Change: v5 defaults to DQL_STRICT_MODE=false, which fundamentally changes how DQL queries behave.
  • v4 default: Objects treated as REGISTER (whole-object replacement)
  • v5 default: Objects treated as MAP (field-level merging)
This affects the behavior of all DQL SELECT, INSERT, and UPDATE operations.
Choose the appropriate migration path based on your current v4 configuration:
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.
Failing to set strict mode will cause objects to merge at the field level instead of replacing entirely, which can result in unexpected data behavior and perceived data loss.
To migrate to DQL_STRICT_MODE=false (the new v5 default), contact Ditto Customer Support for guidance.
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.Recommended: Remove the explicit ALTER SYSTEM SET DQL_STRICT_MODE = false statement entirely β€” the v5 default already matches your intent, so the line is now redundant.
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:
  1. In v4: Set DQL_STRICT_MODE=false after initialization:
  2. Convert all queries from Legacy Query Builder to DQL See the Swift Legacy→DQL Migration Guide for detailed conversion examples.
  3. Upgrade to v5 No DQL configuration changes requiredβ€”v5 defaults to DQL_STRICT_MODE=false.
For additional guidance or questions, contact Ditto Customer Support.

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 core registerObserver API pattern is the same in both versions, returning a DittoStoreObserver that you retain and call .cancel() on for cleanup.
Key changes:
  • v5 adds Codable argument overloads for registerObserver
  • Call item.dematerialize() after extracting data from DittoQueryResultItem to free native memory
  • The observer lifecycle pattern (retain, .cancel(), cleanup) is the same as v4

Back Pressure (Signal Next)

The standard registerObserver 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.
When to use signal next:
  • 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

DittoPeer Property Renames (v5)

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:

Remove Retroactive Sendable Conformances

v5 declares Sendable in-module for its public types, including Ditto, DittoQueryResult, DittoTransactionCompletionAction, and DittoStoreObserver. If your v4-era code added its own retroactive conformance to satisfy Swift 6 strict concurrency, it now conflicts with the SDK’s declaration and fails to compile: Error: conformance of 'X' to protocol 'Sendable' was already stated in the type's module 'DittoSwift' Fix: Delete the retroactive shim:

DittoLogger Changes

DittoLogger retains the same class name in both v4 and v5. The key changes are:
  • v5 marks DittoLogger as final and Sendable for Swift concurrency safety
  • DittoLogger.enabled is renamed to DittoLogger.isEnabled β€” DittoLogger.enabled = false becomes DittoLogger.isEnabled = false. Leaving the old name produces a compile error: type 'DittoLogger' has no member 'enabled'
  • DittoLogger.setLogFile() is removed in v5 (was deprecated in v4 β€” use export(to:) instead)

API Changes

WebSocket Sync Default Changed

DittoHTTPListenConfig.webSocketSync default changed from true to false. If your app relied on the v4 default, you must now explicitly enable it.

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.

New APIs in v5

  • Ditto.open(config:): async throws -> Ditto β€” async factory method
  • Ditto.openSync(config:): throws -> Ditto β€” synchronous factory alternative
  • ditto.config: DittoConfig β€” read the config used to open this instance
  • ditto.isActivated: Bool β€” replaces ditto.activated
  • DittoStore.newAttachment(data: Data, metadata:) β€” create attachment from in-memory Data (v4 was file path only)
  • DittoStore.registerObserver(query:arguments: some Codable, ...) β€” overloads accepting Codable query arguments
  • DittoSync.registerSubscription(query:arguments: some Codable) β€” overload accepting Codable arguments
  • DittoDiskUsageObserver β€” standalone observer class (was nested DiskUsage.DiskUsageObserverHandle)
  • DittoDiskUsageItem β€” renamed from DiskUsageItem
  • DittoSyncSubscription.queryArgumentsCBORData / .queryArgumentsJSONData / .id β€” new properties for introspection
  • DittoConnection now conforms to Identifiable, Equatable, Hashable, and Sendable
  • All public types now final and Sendable for Swift concurrency
  • DittoConfig and DittoConfigConnect now conform to Sendable and Codable

Migration Checklist

Initialization

  • Replace Ditto(identity:) with Ditto.open(config:)
  • Create DittoConfig with databaseID and connect mode
  • Add try await for async initialization
  • Update .onlinePlayground β†’ .server(url:)
  • Update .offlinePlayground β†’ .smallPeersOnly(privateKey: nil)
  • Remove updateTransportConfig calls
  • Remove disableSyncWithV3() calls
  • Set DQL_STRICT_MODE=true BEFORE starting sync if maintaining v4 behavior
  • Update startSync() β†’ sync.start()

Authentication

  • Set expirationHandler closure after initialization
  • Use .development provider for playground tokens
  • Remove authentication from identity configuration

Observers

  • Call item.dematerialize() after extracting data from DittoQueryResultItem to free native memory
  • If processing is expensive, use handlerWithSignalNext: for back-pressure control
  • v5 adds Codable argument overloads β€” adopt where convenient

Data Operations

  • Replace all .collection("x").find(...) chains with store.execute() DQL
  • Use parameterized queries with :paramName β€” never string interpolation
  • Replace .upsert() with INSERT INTO ... ON ID CONFLICT DO UPDATE
  • Replace .update {} closures with UPDATE SET DQL
  • Replace .remove() with DELETE FROM
  • Replace .evict() with EVICT FROM
  • Replace .observeLocal {} with store.registerObserver() callback
  • Replace .subscribe() with sync.registerSubscription()
  • Replace counter?.increment(by:) with UPDATE APPLY ... PN_INCREMENT BY

Breaking Changes

  • Set persistenceDirectory if maintaining v4 directory structure
  • Update peerKeyString β†’ peerKey
  • Update diskUsage usage: DiskUsage β†’ DittoDiskUsage, .exec β†’ .item, DiskUsageObserverHandle β†’ DittoDiskUsageObserver
  • Update DittoConnection.peer1/peer2 comparisons: now String instead of Data
  • Update isConnectedToDittoCloud β†’ isConnectedToDittoServer
  • Update ditto.observePeers(_:) β†’ ditto.presence.observe(didChangeHandler:)
  • Update ditto.activated β†’ ditto.isActivated
  • Remove all DittoSwiftError references β€” use DittoError directly
  • Update DittoSmallPeerInfo.metadataJSONString β†’ metadataJSONData
  • Check for explicit webSocketSync = true if your app relied on v4 default
  • Update DittoLogger.enabled β†’ DittoLogger.isEnabled
  • Update DittoLogger.setLogFile() calls β€” use export(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

  1. DQL_STRICT_MODE silent change: Not setting DQL_STRICT_MODE=true when your v4 app used the default. Objects that were replaced whole will now merge at field level β€” causes unexpected data merging.
  2. Forgetting try await: Ditto.open(config:) is async. Not awaiting it causes a compile error.
  3. Storing DittoQueryResultItem outside callback: These hold native memory. Always call dematerialize() and extract your model before the callback returns.
  4. String interpolation in queries: Never "SELECT * FROM cars WHERE color = '\(color)'". Always use arguments: ["color": color].
  5. Counter initialization: Don’t put DittoCounter() or 0 in insert documents. Counters are created on first PN_INCREMENT. Inserting 0 creates a REGISTER, not a COUNTER.
  6. Missing ON ID CONFLICT: INSERT fails if document _id already exists without a conflict clause.
  7. Not checking timeUntilExpiration in auth handler: Handler is called for both initial auth (0) and token refresh (>0). Handle both cases.