-
Notifications
You must be signed in to change notification settings - Fork 0
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
- What Swift 6 Strict Concurrency Means
- DesignFoundation's Compliance Posture
- Sendable Conformances
- @MainActor Boundaries
- Writing Swift 6-Compatible Code with DesignFoundation
- Patterns and Pitfalls
- Migrating an Existing Project to Swift 6
- Minimum Toolchain Requirements
- FAQ
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.
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-
Sendabletype alongside aDFButtonclosure), not in the library.
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 ✓
}
}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.
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.
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 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
}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.
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 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 Task — Binding is not Sendable.
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)
}
}| 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 |
// 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.
In Xcode, set the Swift Language Version to Swift 6 on one target at a time:
.target(
name: "MyUtilities",
swiftSettings: [.swiftLanguageVersion(.v6)]
),Because DesignFoundation is already Swift 6 clean, adding it to a Swift 6 target is safe. No @preconcurrency wrapping is needed.
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 { ... }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.
| 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.
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.
No. DesignFoundation compiles at Swift 6 language mode, which requires Swift concurrency to be available in the toolchain.
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.
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.