Hi everyone,
I’ve been working on migrating my app (SwimTimes, which helps swimmers track their times) to use Core Data + CKSyncEngine with Swift 6.
After many iterations, forum searches, and experimentation, I’ve created a focused sample project that demonstrates the architecture I’m using.
The good news:
👉 I believe the crashes I was experiencing are now solved, and the sync behavior is working correctly.
👉 The demo project compiles and runs cleanly with Swift 6.
However, before adopting this as the final architecture, I’d like to ask the community (and hopefully Apple engineers) to validate a few critical points, especially regarding Swift 6 concurrency and Core Data contexts.
Architecture Overview
- Persistence layer:
Persistence.swift
sets up the Core Data stack with a main viewContext and a background context forCKSyncEngine
. - Repositories: All Core Data access is abstracted into repository classes (
UsersRepository
,SwimTimesRepository
), with async/await methods. - SyncEngine: Wraps
CKSyncEngine
, handles system fields, sync tokens, and bridging between Core Data entities and CloudKit records. - ViewModels: Marked
@MainActor
, exposing@Published
arrays for SwiftUI. They never touch Core Data directly, only via repositories. - UI: Simple SwiftUI views bound to the ViewModels.
Entities:
UserEntity
→ represents swimmers.SwimTimeEntity
→ times linked to a user (1-to-many).
Current Status
The project works and syncs across devices. But there are two open concerns I’d like validated:
-
Concurrency & Memory Safety
- Am I correctly separating
viewContext
(main/UI) vs. background context (used byCKSyncEngine
)? - Could there still be hidden risks of race conditions or memory crashes that I’m not catching?
- Am I correctly separating
-
Swift 6 Sendable Compliance
- Currently, I still need
@unchecked Sendable
in the SyncEngine and repository layers. - What is the recommended way to fully remove these workarounds and make the code safe under Swift 6’s stricter concurrency rules?
- Currently, I still need
Request
- Please review this sample project and confirm whether the concurrency model is correct.
- Suggest how I can remove the
@unchecked Sendable
annotations safely. - Any additional code improvements or best practices would also be very welcome — the intention is to share this as a community resource.
I believe once finalized, this could serve as a good reference demo for Core Data + CKSyncEngine + Swift 6, helping others migrate safely.
Environment
- iOS 18.5
- Xcode 16.4
- macOS 15.6
- Swift 6
Sample Project
Here is the full sample project on GitHub:
👉 [https://github.com/jarnaez728/coredata-cksyncengine-swift6]
Thanks a lot for your time and for any insights!
Best regards,
Javier Arnáez de Pedro
The first two errors I see are:
- Stored property 'defaults' of 'Sendable'-conforming class 'SyncEngine' has non-Sendable type ‘UserDefaults’
- Stored property '_engine' of 'Sendable'-conforming class 'SyncEngine' is mutable
The error messages clearly pointed out that it's because a Sendable
class can’t have a member variable of a non-Sendable type, and can’t have a mutable state.
Fixing the first error is easy: You can remove the member variable defaults
, and use UserDefaults. standard
instead when needed.
One idea to fix the second error is that you can make _engine
immutable and initialize it in SyncEngine.init
, as shown below, but whether this is appropriate or not depends on how you'd use engine
:
final class SyncEngine {
...
//private let _engine: CKSyncEngine? // Remove this.
private let engine: CKSyncEngine
init(context: NSManagedObjectContext, engine: CKSyncEngine? = nil) {
self.context = context
self.engine = engine
}
}
The other option is to make SyncEngine
an actor, which will protect the mutable state, but you will then need to handle the cross-actor access to the public member variables and methods.
To propose a solution, I’d need to digest your current architecture, and move the discussion to that level, which I’m afraid I don’t have enough bandwidth to do.
Given that, my suggestion will be that you start with the foundational concepts of Swift 6 concurrency, then review your architecture and decide what data will be accessed concurrently (and hence needs to be protected), and go ahead to make it isolated with an actor. If you see a specific pattern that prevents you from going ahead, bring it up here with details for folks to comment (without the need of reading through your whole project). I think that will probably be a more actionable way to move forward.
Best,
——
Ziqiao Chen
Worldwide Developer Relations.