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 Sendableor must be converted to value types - Closure captures require extra attention
Actor Isolation
We refactored several core managers into Actors:
WidgetStore→actor WidgetStoreThemeManager→@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:
- Enable strict concurrency checks at the module level first
- Start fixing from the bottom-level utility libraries
- Gradually work up to the business logic layer
- 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
nonisolatedwisely: 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.