Tech2/15/2026·8 min

Practical Insights on Swift 6 Concurrency

Swift 6's strict concurrency checks brought us plenty of challenges, but also elevated our code quality to a new level. This post shares the problems we encountered during migration and our final solutions.

Why Upgrade to Swift 6

Swift 6 introduced strict concurrency safety checks, meaning the compiler can detect potential data races at compile time. For an app like Craft Widgets that involves extensive asynchronous operations, this was an upgrade we couldn't ignore.

Key Challenges During Migration

Sendable Protocol

One of the biggest changes was the enforcement of the Sendable protocol. Many of our Model types needed to be marked as Sendable, which meant:

  • All stored properties must be Sendable
  • Class types need @unchecked Sendable or must be converted to value types
  • Closure captures require extra attention

Actor Isolation

We refactored several core managers into Actors:

  • WidgetStoreactor WidgetStore
  • ThemeManager@MainActor class ThemeManager

While this added some syntactic overhead, it completely eliminated the possibility of data races.

Incremental Migration Strategy

Rather than migrating all code at once, we adopted an incremental approach:

  1. Enable strict concurrency checks at the module level first
  2. Start fixing from the bottom-level utility libraries
  3. Gradually work up to the business logic layer
  4. Handle the UI layer last

Lessons Learned

  • Prefer value types: Use structs over classes whenever possible
  • Define clear thread boundaries: Mark all UI-related code with @MainActor
  • Use nonisolated wisely: Explicitly annotate methods that don't involve state
  • Testing is essential: Concurrency bugs often only surface under heavy load

Conclusion

While the upgrade to Swift 6 was painful, the benefits were enormous. Our app stability improved significantly, and code maintainability is much better. We strongly recommend teams still on the fence to start migrating as soon as possible.