Make the iCloud gate patient and branded

Stop giving up on a slow iCloud container: a freshly enabled iCloud Drive
that reports nil while still provisioning is now polled patiently for up
to ~10 minutes instead of failing fast to the "unavailable" screen. A nil
ubiquity token (not signed into iCloud at all) still fails immediately,
and the connecting screen reveals an escape hatch at 28s for users who'd
rather jump to setup than keep waiting.

The connecting and iCloud-required screens are now branded — a purple
gradient with teal accents — and the spinner becomes a custom comet-arc
ConnectingIndicator around an iCloud glyph. Connecting copy escalates
with the wait so a slow first connect reads as steady progress.

Claude-Session: https://claude.ai/code/session_01SCv7zvGFcKy47KSTnTLxRe
This commit is contained in:
2026-06-21 14:37:30 -04:00
parent e00b97d587
commit 208fa73f3d
3 changed files with 352 additions and 19 deletions
+62 -10
View File
@@ -31,13 +31,20 @@ final class SyncEngine {
private var monitorTask: Task<Void, Never>?
private var connectAttempt = 0
/// How long `connect()` keeps polling for a still-provisioning iCloud
/// container before falling to the end-of-the-line gate. Deliberately long
/// (~10 min): as long as the user is signed into iCloud, a container that's
/// slow to come online should never be misreported as unavailable. Impatient
/// users bail sooner via the connecting screen's escape hatch (28s).
private static let connectTimeoutSeconds: TimeInterval = 600
private var context: ModelContext { modelContainer.mainContext }
init(container: ModelContainer) {
self.modelContainer = container
}
// MARK: - Connection (deferred, time-boxed)
// MARK: - Connection (deferred, patient)
func connect() async {
guard iCloudStatus != .available else { return }
@@ -46,29 +53,64 @@ final class SyncEngine {
iCloudStatus = .checking
log.info("connect[\(attempt)]: resolving container \(Self.containerIdentifier, privacy: .public)")
let url = await Task.detached {
FileManager.default.url(forUbiquityContainerIdentifier: Self.containerIdentifier)
// Definitive failure first: if the user isn't signed into iCloud at all,
// no container is ever coming go straight to the end-of-the-line gate
// rather than spinning. (Signed-in-but-Drive-off still reports a token;
// that case falls through to the patient poll below and the escape hatch.)
let signedIn = await Task.detached {
FileManager.default.ubiquityIdentityToken != nil
}.value
guard let containerURL = url else {
log.error("connect[\(attempt)]: ubiquity container URL is nil → unavailable (iCloud Drive off, or container not provisioned)")
if attempt == connectAttempt { iCloudStatus = .unavailable }
guard attempt == connectAttempt else { return }
guard signedIn else {
log.error("connect[\(attempt)]: not signed into iCloud → unavailable")
iCloudStatus = .unavailable
return
}
// Signed in, but the container may still be provisioning common right
// after enabling iCloud Drive. Keep polling patiently: we'd rather hold the
// spinner than misreport a working account as unavailable. We only give up
// after a considerable timeout; the user can bail sooner via the connecting
// screen's escape hatch (which bumps connectAttempt and stops this loop).
var resolved: URL?
let deadline = Date().addingTimeInterval(Self.connectTimeoutSeconds)
while resolved == nil {
let url = await Task.detached {
FileManager.default.url(forUbiquityContainerIdentifier: Self.containerIdentifier)
}.value
guard attempt == connectAttempt else { return }
if let url {
resolved = url
break
}
if Date() >= deadline {
log.error("connect[\(attempt)]: container still nil after \(Int(Self.connectTimeoutSeconds))s → unavailable")
iCloudStatus = .unavailable
return
}
log.info("connect[\(attempt)]: container nil — still provisioning, retrying")
try? await Task.sleep(for: .seconds(2))
guard attempt == connectAttempt else { return }
}
guard let containerURL = resolved else { return }
log.info("connect[\(attempt)]: container URL = \(containerURL.path, privacy: .public)")
let fm = ICloudFileManager(containerURL: containerURL)
let timeout = Task { [weak self] in
try? await Task.sleep(for: .seconds(20))
// Safety net only: prepareDirectories is a local op that effectively never
// blocks, but if the first container file op ever wedges we don't want an
// eternal spinner. This is generous it isn't the connect path's clock.
let safety = Task { [weak self] in
try? await Task.sleep(for: .seconds(30))
guard let self, !Task.isCancelled else { return }
if self.iCloudStatus == .checking, attempt == self.connectAttempt {
self.log.error("connect[\(attempt)]: timed out after 20s — first container file op blocked (dataless container?)")
self.log.error("connect[\(attempt)]: prepareDirectories wedged 30s → unavailable")
self.iCloudStatus = .unavailable
}
}
log.info("connect[\(attempt)]: preparing directories…")
await fm.prepareDirectories()
timeout.cancel()
safety.cancel()
guard attempt == connectAttempt else { return }
self.fileManager = fm
@@ -81,6 +123,16 @@ final class SyncEngine {
cleanupOldStubs()
}
/// Invoked from the connecting screen when the user chooses not to keep
/// waiting. Bumps `connectAttempt` to stop the in-flight poll loop, then drops
/// to the end-of-the-line gate (with its Try Again).
func abandonWaiting() {
guard iCloudStatus == .checking else { return }
connectAttempt += 1
iCloudStatus = .unavailable
log.info("connect: abandoned by user → unavailable")
}
// MARK: - Monitoring
private func startMonitoring(documentsURL: URL) {