Core Data + CKSyncEngine with Swift 6 — concurrency, Sendable, and best practices validation

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 for CKSyncEngine.
  • 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:

  1. Concurrency & Memory Safety

    • Am I correctly separating viewContext (main/UI) vs. background context (used by CKSyncEngine)?
    • Could there still be hidden risks of race conditions or memory crashes that I’m not catching?
  2. 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?

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

Answered by DTS Engineer in 860451022

The first two errors I see are:

  1. Stored property 'defaults' of 'Sendable'-conforming class 'SyncEngine' has non-Sendable type ‘UserDefaults’
  2. 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.

Thanks for writing the post and being willing to share knowledge with the community. I probably don't have enough cycles to review the project. Also, providing best practices on the whole thing will be hard, because synchronization is complicate, and there are corner cases that need to be handled carefully.

I think I can start with some specific aspects though, with the hope that other folks can jump in.

In general, using viewContext for UI and background contexts for background synchronization is the right pattern. I don't have more to say before thoroughly reading your code or seeing an issue.

If you can elaborate a bit about the concrete cases where you need to use @unchecked Sendable and why, I may be able to comment.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Thanks, @DTS Engineer.

Opening the project in Xcode 26 already shows some warnings in SyncEngine.swift. We can start there. I don’t want to use @unchecked Sendable.

Could you please:

  1. Explain how I can remove the warnings?
  2. Show how to remove @unchecked Sendable from SyncEngine.swift? If I just delete it, multiple errors appear.

The first two errors I see are:

  1. Stored property 'defaults' of 'Sendable'-conforming class 'SyncEngine' has non-Sendable type ‘UserDefaults’
  2. 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.

Core Data + CKSyncEngine with Swift 6 — concurrency, Sendable, and best practices validation
 
 
Q