Skip to content

Swift 6 Concurrency

Daniel S edited this page Jul 2, 2026 · 1 revision

Swift 6 Concurrency

DesignFoundation is fully compiled at Swift 6 language mode — zero warnings, zero @preconcurrency suppressions in public API. This page explains what that means, how the package achieves it, and how to write Swift 6-compatible code that uses DesignFoundation without introducing data-race warnings of your own.

See also: Theming · Components-Overview · Custom-Styles · Form-Validation


Table of Contents

  1. What Swift 6 Strict Concurrency Means
  2. DesignFoundation's Compliance Posture
  3. Sendable Conformances
  4. @MainActor Boundaries
  5. Writing Swift 6-Compatible Code with DesignFoundation
  6. Patterns and Pitfalls
  7. Migrating an Existing Project to Swift 6
  8. Minimum Toolchain Requirements
  9. FAQ

What Swift 6 Strict Concurrency Means

Swift 6 promotes the concurrency safety rules that were optional diagnostics in Swift 5.5–5.9 into hard compiler errors. The goal is to make data races impossible to express — not merely warned about, but literally uncompilable.

The three core rules that become enforced errors at Swift 6 language mode are:

Rule What the compiler rejects
Sendable isolation Passing a non-Sendable value across actor boundaries (e.g. from a Task to the main actor)
Actor-isolation violations Calling a @MainActor-isolated function from a non-isolated async context without await
Closure capture safety Capturing a mutable non-Sendable reference in a @Sendable closure

Prior to Swift 6, you opted into these diagnostics via build settings → SWIFT_STRICT_CONCURRENCY = complete. Under Swift 6, they are always on, for every file compiled at that language version.

A package declares its language version in Package.swift:

// Package.swift
let package = Package(
    name: "DesignFoundation",
    // ...
    targets: [
        .target(
            name: "DesignFoundation",
            swiftSettings: [
                .swiftLanguageVersion(.v6)   // ← hard Swift 6 mode
            ]
        )
    ]
)

DesignFoundation declares swiftLanguageVersions: [.v6] at the package level. Every source file in the library compiles clean under these rules.


DesignFoundation's Compliance Posture

The design goal was zero suppression — no @preconcurrency import, no nonisolated(unsafe), no @unchecked Sendable on types that actually share mutable state. Every public type earns its Sendable conformance structurally or via explicit, safe annotation.

What this means for you as a consumer:

  • Importing DesignFoundation into a Swift 6 target produces no warnings from the library.
  • All public API is annotated such that the compiler can prove data-race safety at call sites.
  • If you get a concurrency warning involving a DesignFoundation type, the issue is in your code (e.g. capturing a non-Sendable type alongside a DFButton closure), not in the library.

Sendable Conformances

Theme and Token Types

DFTheme and all token subtypes are value-type structs. In Swift 6, a struct is implicitly Sendable when all of its stored properties are themselves Sendable. Because every property bottoms out in Color, Font, CGFloat, Double, Bool, and String — all Sendable — the compiler synthesizes Sendable for free.

All token types carry an explicit conformance declaration in the public interface for clarity and API-contract stability:

public struct DFTheme: Sendable { ... }
public struct DFColorTokens: Sendable { ... }
public struct DFSpacingTokens: Sendable { ... }
public struct DFRadiusTokens: Sendable { ... }
public struct DFTypographyTokens: Sendable { ... }
public struct DFAnimationTokens: Sendable { ... }
public struct DFShadowTokens: Sendable { ... }
public struct DFMaterialTokens: Sendable { ... }

DFThemePreset is an enum with associated values that are all DFTheme instances — value types — so it is also Sendable:

public enum DFThemePreset: Sendable {
    case `default`, slate, copper, aurora, sage
    public var theme: DFTheme { ... }
}

This means you can freely pass themes and presets across actor boundaries — into Task {} bodies, as actor stored properties, as arguments to async functions — without any suppression or bridging.

// Sending a theme into a background actor — completely safe
actor ThemeStore {
    var activeTheme: DFTheme  // Sendable ✓

    func applyPreset(_ preset: DFThemePreset) {
        activeTheme = preset.theme  // Sendable ✓
    }
}

Component Data Types

Beyond the theme layer, every data type you construct to configure a DesignFoundation component is Sendable:

Type Kind Sendable mechanism
DFAlert struct Synthesized (all fields Sendable)
DFAlertAction struct Explicit (closure annotated @Sendable)
DFAlertActionRole enum Synthesized
DFToastMessage struct Explicit
DFToastSeverity enum Synthesized
DFToastAction struct Explicit (closure annotated @Sendable)
DFFormState struct Explicit
DFFieldValidator struct Explicit
DFPlatformContext struct Explicit
DFPlatformVariant enum Synthesized
DFSidebarSection struct Synthesized
DFSidebarItem struct Synthesized
DFTabItem struct Synthesized

All style protocol requirements also mandate Sendable — see Style-System for details and the next section for an example of what happens when you forget.

@Sendable Closures

Every action closure in DesignFoundation's public API is annotated @Sendable. For example:

// Simplified public signatures (illustrative)
public struct DFButton: View {
    public init(_ label: String, action: @escaping @Sendable () -> Void)
    public init(_ label: String, isLoading: Bool, action: @escaping @Sendable () -> Void)
}

public struct DFAlertAction: Sendable {
    public init(title: String, role: DFAlertActionRole, action: @escaping @Sendable () -> Void)
}

public struct DFToastAction: Sendable {
    public init(label: String, action: @escaping @Sendable () -> Void)
}

The @Sendable annotation on a closure means the compiler will verify that everything you capture in that closure is itself Sendable (or isolated to the same actor). This is where most developer-side concurrency errors surface when first adopting DesignFoundation in a Swift 6 project. See Capturing Self in Button Closures for solutions.


@MainActor Boundaries

DFToastQueue

DFToastQueue is a @MainActor-isolated class. Its published state drives SwiftUI UI updates, which must happen on the main thread. The class declaration is:

@MainActor
public final class DFToastQueue: ObservableObject {
    public static let shared: DFToastQueue
    public func show(_ message: String, style: DFToastSeverity)
    public func show(_ message: DFToastMessage)
}

Because the class and its methods are @MainActor, the compiler guarantees that calls to show() always execute on the main actor. There is no manual DispatchQueue.main.async needed — Swift's actor hopping handles it.

Calling from a SwiftUI view (already on MainActor):

struct SaveButton: View {
    var body: some View {
        DFButton("Save") {
            // SwiftUI action closures are called on the main actor
            DFToastQueue.shared.show("Saved!", style: .success)
        }
    }
}

Calling from inside a Task {} (auto-hop):

When you call a @MainActor method from within a Task, the runtime automatically hops to the main actor before executing the call. No annotation required:

DFButton("Upload") {
    Task {
        await uploadFile()
        // Hop to main actor happens automatically
        DFToastQueue.shared.show("Upload complete", style: .success)
    }
}

Calling from an async function not isolated to MainActor:

If your async function is not @MainActor, you must await the call. The compiler will error if you don't:

func performSync() async {
    await uploadFile()
    // ✗ ERROR: Expression is 'async' but is not marked with 'await'
    // DFToastQueue.shared.show("Done", style: .success)

    // ✓ Correct: explicit await to hop to main actor
    await MainActor.run {
        DFToastQueue.shared.show("Done", style: .success)
    }
}

Calling from a non-async background context (rare):

If you have a completion-handler-based callback that is not on the main thread:

legacySDK.doWork { result in
    Task { @MainActor in
        DFToastQueue.shared.show(result.message, style: .success)
    }
}

The Task { @MainActor in } idiom is the idiomatic bridge from synchronous non-actor code into @MainActor async code.

DFPlatformContext.current

DFPlatformContext.current is a @MainActor static computed property:

public struct DFPlatformContext: Sendable {
    @MainActor public static var current: DFPlatformContext { get }
}

It is @MainActor because it reads UIDevice.current (on iOS/iPadOS) and other UIKit/AppKit properties that are main-thread-only. The struct it returns — DFPlatformContext — is Sendable, so once you have the value you can pass it anywhere.

In practice, you rarely need to call DFPlatformContext.current directly. DesignFoundation injects it automatically via the .dfTheme() environment modifier. The only times you would call it are:

  • Writing a custom component that branches on platform layout
  • Writing a custom style type that needs to know the platform at style-resolution time
// ✓ Reading from a @MainActor context (SwiftUI body, @MainActor func)
struct AdaptiveRow: View {
    @Environment(\.dfTheme) private var theme

    var body: some View {
        let context = DFPlatformContext.current  // safe: body runs on main actor
        HStack(spacing: context.variant == .compact ? theme.spacing.sm : theme.spacing.lg) {
            // ...
        }
    }
}

// Reading from async context
func logPlatform() async {
    let context = await MainActor.run { DFPlatformContext.current }
    print(context.variant)  // Sendable — safe to use off main actor after capture
}

Writing Swift 6-Compatible Code with DesignFoundation

Custom Style Types Must Be Sendable

All style protocols in DesignFoundation include Sendable in their protocol requirements. If you define a custom style and forget Sendable, the compiler will produce an error at the call site where you apply the style modifier.

Wrong — missing Sendable (compiler error):

// MyButtonStyle.swift
struct MyButtonStyle: DFButtonStyle {
    // ✗ ERROR: Type 'MyButtonStyle' does not conform to protocol 'Sendable'
    // because stored property 'manager' is of non-Sendable type 'AnimationManager'
    let manager: AnimationManager  // AnimationManager is a class, not Sendable

    func makeBody(configuration: Configuration) -> some View { ... }
}

Correct — two options:

Option A: Make AnimationManager sendable (preferred if you own it):

final class AnimationManager: @unchecked Sendable { ... }

struct MyButtonStyle: DFButtonStyle {
    let manager: AnimationManager  // Now Sendable ✓
    func makeBody(configuration: Configuration) -> some View { ... }
}

Option B: Eliminate the stored reference — pass only Sendable configuration values:

struct MyButtonStyle: DFButtonStyle {
    let animationDuration: Double  // Sendable ✓
    let springDamping: Double      // Sendable ✓

    func makeBody(configuration: Configuration) -> some View { ... }
}

See Style-System for a full guide to implementing style protocols.

Capturing Self in Button Closures

The most common concurrency warning when using DesignFoundation in a Swift 6 app is capturing self in a DFButton action closure when self is a non-Sendable class (e.g. an ObservableObject view model).

Solution A — [weak self] + Task { @MainActor in }:

DFButton("Save") { [weak vm] in
    guard let vm else { return }
    Task { @MainActor in
        await vm.saveDocument()
    }
}

Solution B — isolate the view model to @MainActor (recommended for UI-driving view models):

@MainActor
class DocumentViewModel: ObservableObject {
    @Published var isSaving = false
    func saveDocument() async { ... }
}

struct DocumentView: View {
    @StateObject private var vm = DocumentViewModel()

    var body: some View {
        DFButton("Save") {
            // ✓ vm is @MainActor-isolated; DFButton closures run on the main actor
            Task { await vm.saveDocument() }
        }
    }
}

Annotating your view model @MainActor is the cleanest path. It aligns the model's isolation with SwiftUI's main-thread execution model and eliminates the entire category of capture warnings for UI-bound types.

DFFormState in Async Flows

DFFormState is a Sendable value type — it's a struct. This means you can safely pass it across actor boundaries, but mutations do not propagate backward.

The safe pattern for async validation is to snapshot the form state before crossing an async gap:

struct RegistrationView: View {
    @State private var formState = DFFormState()

    var body: some View {
        DFButton("Register") {
            let snapshot = formState  // snapshot — Sendable value ✓
            Task {
                let result = await RegistrationService.submit(snapshot)
                await MainActor.run {
                    if result.success {
                        DFToastQueue.shared.show("Registered!", style: .success)
                    } else {
                        DFToastQueue.shared.show(result.errorMessage, style: .error)
                    }
                }
            }
        }
    }
}

Do not capture $formState (the Binding) in a TaskBinding is not Sendable.

Storing DFTheme in @State

DFTheme is a Sendable value type. Storing it in @State is safe and idiomatic:

struct RootView: View {
    @State private var activeTheme: DFTheme = DFThemePreset.default.theme

    var body: some View {
        ContentView()
            .environment(\.dfTheme, activeTheme)
    }
}

Patterns and Pitfalls

Scenario Correct approach What goes wrong if you don't
Showing a toast from a background Task await MainActor.run { DFToastQueue.shared.show(...) } Compiler error: call to @MainActor function outside main actor
Applying a custom style with a class-typed property Conform the class to @unchecked Sendable or replace with value-typed config Compile error: style type doesn't satisfy Sendable requirement
Passing DFTheme to an async function Pass directly — it's Sendable No issue; this just works
Capturing ObservableObject in a DFButton closure Mark view model @MainActor or use [weak self] Warning or error: capture of non-Sendable type in @Sendable closure
Reading DFPlatformContext.current from a Task let ctx = await MainActor.run { DFPlatformContext.current } Compiler error: @MainActor-isolated property accessed from non-isolated context
Storing form state across an async gap Copy to local let before the Task Potential stale data if @State mutates; Binding not sendable
Using DFThemePreset as an actor stored property Just do it — it's Sendable No issue; this just works

Migrating an Existing Project to Swift 6

Step 1 — Audit your current warnings at complete mode

// Xcode build settings
SWIFT_STRICT_CONCURRENCY = complete

Fix all resulting warnings. This is the same work Swift 6 requires, but surfaced as warnings rather than errors.

Step 2 — Enable Swift 6 per-target, not project-wide

In Xcode, set the Swift Language Version to Swift 6 on one target at a time:

.target(
    name: "MyUtilities",
    swiftSettings: [.swiftLanguageVersion(.v6)]
),

Step 3 — Add DesignFoundation

Because DesignFoundation is already Swift 6 clean, adding it to a Swift 6 target is safe. No @preconcurrency wrapping is needed.

Step 4 — Annotate your view models @MainActor

The single highest-yield change in a SwiftUI app is marking all ObservableObject view models @MainActor. This resolves the vast majority of capture-in-closure warnings you will encounter when using DesignFoundation's action closures.

// Before
class ProfileViewModel: ObservableObject { ... }

// After — resolves all capture warnings in SwiftUI views that use this VM
@MainActor
class ProfileViewModel: ObservableObject { ... }

Step 5 — Migrate style types

If you have existing custom component styles, add Sendable conformance. For most styles this is automatic because they contain only value-typed configuration. If a style holds a reference-typed dependency, refactor to pass configuration values instead of object references.


Minimum Toolchain Requirements

Requirement Minimum version
Swift 6.0
Xcode 16.0
iOS deployment target 18.0
macOS deployment target 15.0
visionOS deployment target 2.0

DesignFoundation's Package.swift sets:

swiftLanguageVersions: [.v6]

This means SPM will refuse to resolve the package into a target compiled at Swift 5.x. If you see a resolution error like package 'DesignFoundation' requires minimum Swift language version 6, the fix is to set SWIFT_VERSION = 6 on the consuming target.


FAQ

Does DesignFoundation use actors internally?

Not custom actors. The library uses @MainActor isolation on types whose behaviour is inherently UI-bound (DFToastQueue, DFPlatformContext.current). All other types are value types (Sendable structs and enums) that carry no isolation requirement.

Can I use DesignFoundation with Swift concurrency turned off?

No. DesignFoundation compiles at Swift 6 language mode, which requires Swift concurrency to be available in the toolchain.

What if my app is on Swift 5.9 or 5.10?

DesignFoundation requires Swift 6.0 / Xcode 16+. You cannot link DesignFoundation into a target compiled at Swift 5.x — SPM will produce a resolution error.

Why are action closures @Sendable rather than plain closures?

A plain () -> Void closure in Swift 6 is not implicitly Sendable. By annotating closures @Sendable up front, the library pushes the verification burden to the call site — where it belongs — and keeps the library's internal implementation safe by construction.


This page covers DesignFoundation's Swift 6 concurrency model as of the current release. For component-level API details, see Components-Overview. For custom styling guidance, see Style-System. For form validation integration, see Forms-and-Validation.

Clone this wiki locally