kostia.dev
Swift
Concurrency
Swift 6 Migration
Refactoring
Sendable
Region-Based Isolation

Refactoring for Swift 6 Concurrency

Jun 13, 2025
5 min read

Practical refactoring tips for migrating to Swift 6 concurrency: @Sendable, sending, region-based isolation, and common error fixes.

When migrating to Swift 6 and adopting @Sendable, sending, and region-based isolation, you're likely to run into a series of compiler errors. Here's how you can identify patterns and apply effective refactors.


πŸ” Common Migration Errors & How to Fix Them

❌ Error: Sending value of non-Sendable type '() async throws -> ()' risks causing data races

Before:

swift
class User { // User properties and methods var id: String = "12345" } func buildUserProfile() async throws { let user = User() Task { [user] in try await sendAnalyticsEvents(for: user) // ❌ } user.id = "67890" } func sendAnalyticsEvents(for user: User) async throws { // Simulate sending analytics events print("Sending analytics events for user with ID: (user.id)") }

After: Think of models rather as immutable states, structures

swift
// Making a struct instead of a class struct User { var id: String = "12345" } func buildUserProfile() async throws { var user = User() Task { [user] in try await sendAnalyticsEvents(for: user) // βœ… this will be captured value } user.id = "67890" // no affect on the sent value }

Or you can make user a Sendable type

swift
// Making User conform to Sendable: final and immutable final class User: Sendable { let id: String = "12345" } func buildUserProfile() async throws { let user = User() Task { [user] in try await sendAnalyticsEvents(for: user) // βœ… this will be captured value } // user.id = "67890" // will not compile, as User is immutable }

❌ Error: Sending 'self.viewModel' risks causing data races

Before:

swift
final class ViewModel { func loadItems() async throws -> [String] { try await Task.sleep(nanoseconds: 1_000_000_000) // 1 second delay return ["Item 1", "Item 2", "Item 3"] } } struct ContentView: View { var viewModel = ViewModel() var body: some View { EmptyView() .task { _ = try? await viewModel.loadItems() // ❌ } } }

Fix: Marking viewModel as @MainActor to match the view's actor context.

swift
@MainActor βœ… final class ViewModel { ...

Or isolate your logic in a @MainActor wrapper function.

❌ Error: Static property 'shared' is not concurrency-safe because non-'Sendable' type 'DateFormatterCache' may have shared mutable state

Before:

swift
class DateFormatterCache { static let shared = DateFormatterCache() // ❌ }

Fix: use an actor and mark methods as needed actors

swift
actor DateFormatterCache { static let shared = DateFormatterCache() // βœ… @MainActor // βœ… func format(date: Date) -> String { let formatter = DateFormatter() formatter.dateStyle = .medium return formatter.string(from: date) } }

πŸ› οΈ General Refactoring Strategies

βœ… Use @MainActor/@globalActor

Helps isolate UI logic and avoids implicit captures.

🧡 Adopt sending for closures

Where applicable, use the sending keyword for closure parameters to express safe, region-based logic.

🧰 Extract Logic from Closures

Move complex operations or risky captures into named functions or actor contexts.

🚦 Reduce Concurrency Where Possible

Instead of scattering Task everywhere, centralize async logic or defer it through controlled actors or services.

πŸ”’ Use @unchecked Sendable Cautiously

Only when you're confident in thread-safety and isolation. Mark explicitly and document your reasoning.


🧭 Checklist for Migrating Code

  • Audit all Task and closure captures for Sendable compliance
  • Use @MainActor for UI and view model logic
  • Refactor shared mutable state to actors or thread-safe patterns
  • Prefer region-based isolation and sending where possible
  • Document all uses of @unchecked Sendable
  • Run tests for concurrency bugs after migration

By applying these patterns early, you can turn the Swift 6 migration into a progressive hardening of your architecture β€” not just a compiler whack-a-mole.


πŸ”§ Treat errors as opportunities to make code more explicit, more isolated, and safer by design.

Related Posts

Swift
Concurrency
iOS
A guide to region-based isolation, @Sendable, sending and unchecked sendable, and closure safety in Swift 6 with examples.
6/12/2025
7 min read
2 shared tags
Swift
Concurrency
iOS
A deep dive into Swift's concurrency model, focusing on actors and region-based isolation.
6/10/2025
9 min read
2 shared tags
iOS
Swift
Performance
Swift implements four types of method dispatch: inlining, static, table, and message. Understanding which one applies and when explains some of Swift's most confusing behaviour β€” and helps you write faster code.
2/21/2026
10 min read
1 shared tag