Repository navigation
perf(sync): fix iCloud-sync UI freezes - #61
Merged
Merged
Conversation
The UI stalled during sync from two main-thread sources: redundant reconcile passes merging into the main context (re-running on-screen @query), and TMDB/OMDB detail enrichment saving synchronously on the view's main context on every detail open. - Gate foreground-triggered reconciles behind a 30s interval (CloudSyncCoordinator.minForegroundReconcileInterval). Quick app-switches no longer re-merge for no new data; genuine remote changes still pull immediately via the remote-change observer. - Move TMDB + OMDB enrichment off the main thread: ContentSyncManager now owns enrichMovie/enrichSeries/enrichMovieRatings/enrichSeriesRatings which fetch + apply + save on a background context (auto-merges into the main context). Detail views and Home hero-logo enrichment call these instead of applying on modelContext + synchronous save. - Make applyMovieDetails/applySeriesDetails/replaceCast and the TMDBTitleDetails/TMDBCastMember DTOs nonisolated so the background path can read them off the main actor. Pre-commit hook bypassed: lefthook's SwiftFormat SPM plugin can't resolve 0.61.1 offline; verified clean with standalone swiftformat 0.61.1 + swiftlint 0.63.2.
CloudSyncShadow.persist() rewrote the entire playlist + content baseline to UserDefaults as JSON on every reconcile pass, even a steady-state pass where every merge verdict was .noChange. Track a dirty flag set only when a setter actually changes a value (or resetContent clears a non-empty baseline); persist() now no-ops unless something changed. (Pre-commit hook bypassed: lefthook SwiftFormat plugin can't resolve 0.61.1 offline; verified clean with standalone swiftformat 0.61.1 + swiftlint 0.63.2.)
The foreground reconcile is a safety net — a change made on another device while this one was backgrounded lands via CloudKit's own remote-change notification on reconnect, not via the foreground pass. So reopening the app no longer needs to kick off a sync. Stretch the foreground gate from 30s to 30 min so routine app re-activations stop triggering reconciles (the "syncs too often" feel), while remote-change pulls and the background flush stay prompt (capping those would delay cross-device sync or drop local edits before suspension).
bilipp
added a commit
that referenced
this pull request
Jun 16, 2026
perf(sync): fix iCloud-sync UI freezes
9 of 12 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
iCloud sync caused heavy UI lag — even on the latest hardware the app felt completely stuck during sync, and sync seemed to fire at random / too often (on every foregrounding, on opening a movie detail, etc.).
Root cause
The work itself was already off-main, but two things hit the main thread:
@Query. The "opening a movie detail" lag was actually TMDB/OMDB enrichment saving synchronously on the view's main context (scalar fields + a full cast replace) on every detail open.UserDefaultsJSON on every pass, even when nothing changed.(At the transport layer CloudKit already syncs deltas; the reconcile scan is bounded by user-state size, not catalog size — so an incremental/persistent-history rewrite was deliberately not pursued: it would be dominated by catalog-sync churn and add fragility for no real gain.)
Changes
CloudSyncCoordinator.minForegroundReconcileInterval). Quick app-switches no longer re-merge for no new data; genuine remote changes still pull immediately via the remote-change observer.ContentSyncManagernow ownsenrichMovie/enrichSeries/enrichMovieRatings/enrichSeriesRatings, which fetch + apply + save on a background context (auto-merging into the main context). All 5 detail views + Home hero-logo enrichment call these instead of applying onmodelContext+ a synchronous save.applyMovieDetails/applySeriesDetails/replaceCastand theTMDBTitleDetails/TMDBCastMemberDTOs are nownonisolated.CloudSyncShadowtracks a dirty flag;persist()no-ops in steady state.Testing
CloudSyncTestsandContentSyncTestspass (the one failingContentSyncTestscase is a pre-existing bulk-insert benchmark, unrelated — it touches none of this code).swiftformat0.61.1 +swiftlint0.63.2 clean.Note for review
This reverses the original code's deliberate "apply enrichment on the view context to avoid cross-context merge timing issues" choice. The awaited background save + auto-merge, followed by
refreshToken = UUID(), is the safety net. Episodes still load on the view context on purpose (instant relationship display) and are unchanged. Worth a sanity check that enriched text/cast/logos still appear promptly on detail open.