Status: Living — Apple-side runbook + verification reference. Reader: developer / release engineer maintaining the sync feature (issue #9). Design:
sync-design.md. Sync is opt-in (off by default); the only remaining step is the manual two-device soak in Step 5 below.
The Dart side, the Swift ICloudSyncHandler, and the in-app opt-in UI
(Settings → Sync + the onboarding step, #142) all ship. The build compiles
and runs cleanly on iOS; ICloudSyncTransport.account() returns null and the
orchestrator stays in SyncSignedOut until both the Apple-side configuration
AND a signed-in iCloud account with iCloud Drive on for the app are in place.
Apple-side status (completed in the v1.2.7 / iOS 1.0 release; still
current): steps 1, 2, and 3 below were completed during the v1.2.7
release push — the App ID has iCloud
capability enabled (Xcode 6 / CloudKit-compatible mode), the iCloud
container iCloud.dev.tomhess.crosscue exists, and the App Store
provisioning profile carries the modern icloud-services +
icloud-container-identifiers + ubiquity-container-identifiers
entitlements. The APPLE_PROVISIONING_PROFILE_BASE64 GitHub Secret
holds the regenerated profile. Verify with:
security cms -D -i /path/to/Crosscue_App_Store.mobileprovision \
| grep -A1 -E "icloud-container-identifiers|icloud-services|ubiquity-container-identifiers"
What’s still pending: the manual two-device soak (Step 5 below) — end-to-end verification on two devices signed into the same iCloud account with an entitlement-carrying build. Everything else (transport, handler, opt-in UI) is in place; this guide is the Apple-side on-ramp + verification reference.
This guide walks through the three things that must happen on the Apple side before sync can activate, plus how to verify it end-to-end.
dev.tomhess.crosscue).Crosscue Sync.iCloud.dev.tomhess.crosscue (the iCloud. prefix is
mandatory; the rest matches the app’s bundle id).This container is what the app reads/writes under
<container>/Documents/sync/.
dev.tomhess.crosscue.iCloud.dev.tomhess.crosscue container created in
Step 1.crosscue/ios/Runner.xcworkspace in Xcode.iCloud.dev.tomhess.crosscue.Xcode creates Runner/Runner.entitlements and adds the
CODE_SIGN_ENTITLEMENTS build setting to Runner.xcodeproj for both Debug
and Release configurations. The resulting entitlements file looks like:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.icloud-container-identifiers</key>
<array>
<string>iCloud.dev.tomhess.crosscue</string>
</array>
<key>com.apple.developer.icloud-services</key>
<array>
<string>CloudDocuments</string>
</array>
<key>com.apple.developer.ubiquity-container-identifiers</key>
<array>
<string>iCloud.dev.tomhess.crosscue</string>
</array>
</dict>
</plist>
Commit both files (Runner.entitlements and the modified project.pbxproj).
On a device or simulator signed into iCloud, with Settings → Apple ID → iCloud → iCloud Drive → Crosscue turned on:
// In a debug build or quick test screen:
final transport = ref.read(syncTransportProvider);
final account = await transport.account();
debugPrint('account: $account'); // expect non-null
A non-null result means the handler can see the ubiquity token and the
container directory exists. From there, drive an end-to-end sync via the
orchestrator and confirm files appear in
~/Library/Mobile Documents/iCloud~dev~tomhess~crosscue/Documents/sync/
on the host Mac (for simulator) or in the cloud on a real device.
sync-design.md → Conflict resolution).The handler is async throws (iOS 16+; #113). A missing blob is reported as
nil/empty — the normal “we don’t have it yet” signal. An access failure is
thrown back to Dart as a FlutterError with a structured code, so the
orchestrator never mistakes a locked file for a missing one (which would
trigger a spurious re-upload):
Native FlutterError.code |
When | Dart SyncTransportErrorKind |
Retried automatically? |
|---|---|---|---|
ICLOUD_LOCKED |
NSFileCoordinator couldn’t acquire access (another device holds a claim) |
locked |
yes (transient) |
ICLOUD_QUOTA |
NSFileWriteOutOfSpaceError |
quotaExceeded |
no — user frees space |
ICLOUD_PERMISSION |
NSFileReadNoPermissionError / NSFileWriteNoPermissionError |
permissionDenied |
no |
ICLOUD_IO |
any other I/O error | io |
yes (transient) |
ICloudSyncTransport maps these codes to a SyncTransportException; the
orchestrator turns that into a SyncError(transient:) published on its state
stream (it does not rethrow, so the app-resume / post-solve triggers can’t
raise an uncaught error). Unknown codes (e.g. INVALID_ARGS) keep the previous
graceful-null degradation.
If anything goes wrong, the sync feature is off-by-default at the
SyncOrchestrator level — enable() is only ever called when the user opts
in (Settings → Sync or the onboarding step). Even with the entitlement
attached, no writes happen until the user turns sync on.
To fully revoke: in Xcode → Signing & Capabilities, click the trash icon
next to the iCloud capability. Rebuild. The Swift handler reverts to
returning nil from account() immediately.