Overview
Swift 6 makes concurrency safety a compile-time concern and tightens the rules around isolation. This page covers the defaults a Swift 6 codebase should use. Read general-principles first; the value-type and naming rules below build on those.
Adopt structured concurrency
Use async/await, Task, TaskGroup, and actors. Do not start work that outlives its caller without a clear lifecycle.
func fetchAll(urls: [URL]) async throws -> [Data] {
try await withThrowingTaskGroup(of: Data.self) { group in
for url in urls {
group.addTask { try await URLSession.shared.data(from: url).0 }
}
var results: [Data] = []
for try await data in group { results.append(data) }
return results
}
}Task { ... } at the call site is a fire-and-forget escape hatch. Use it only at boundaries where the caller cannot be async (a SwiftUI button’s action:). Inside async code, pass tasks to a group so cancellation propagates.
Make actor isolation explicit
Mutable state lives inside an actor. Cross-actor access is await-ed. Mark UI-bound types with @MainActor; mark types that mutate independent state with their own actor.
@MainActor
final class FeedViewModel: ObservableObject {
@Published var items: [FeedItem] = []
func load() async { ... }
}
actor RateLimiter {
private var tokens: Int = 100
func take() -> Bool { ... }
}Avoid nonisolated(unsafe). The flag silences the compiler without solving the data race; if you find yourself reaching for it, the type is wrong.
Prefer value types
Reach for struct and enum first. Use class only when reference identity is part of the contract (a view-model an ObservableObject framework needs to track, a stateful service injected throughout the app).
Value types compose, copy cheaply with copy-on-write for collections, and survive concurrency without surprises. A struct that uses let for all stored properties is Sendable for free. Modeling domain data as values eliminates an entire class of aliasing bugs.
Never force-unwrap
! on an optional is a runtime crash waiting for a Monday morning. Use if let, guard let, ??, or pattern matching.
// Bad
let user = users.first!
// Better
guard let user = users.first else {
return .empty
}The exceptions are narrow: IBOutlets wired in Interface Builder, image assets compiled into the bundle, and tests where a missing fixture should fail loudly. In every other case, force-unwrap is a bug.
Errors are explicit, not hidden
Use throws for recoverable failure. Use Result only at boundaries that need to defer error handling (caching a failed call, passing through a callback API). Avoid try? outside expressions where nil genuinely means “skip and continue.”
enum InvoiceError: Error {
case malformed(reason: String)
case unauthorized
case upstreamUnavailable
}
func loadInvoice(id: UUID) async throws -> Invoice {
let (data, response) = try await URLSession.shared.data(from: url(for: id))
guard let http = response as? HTTPURLResponse else { throw InvoiceError.upstreamUnavailable }
switch http.statusCode {
case 200: return try JSONDecoder().decode(Invoice.self, from: data)
case 401: throw InvoiceError.unauthorized
default: throw InvoiceError.upstreamUnavailable
}
}Name the error type with the domain it serves. Boundary code catches and translates; internal code lets the throw propagate. See general-principles on boundary vs internal error handling.
Design around protocols
Define behavior with protocols and add capability with extensions. Inheritance from a base class is the wrong tool in almost every case Swift offers.
- Use a protocol for a capability the caller depends on (
Identifiable,Codable, a domain protocol likeInvoiceStore). - Use protocol extensions for default implementations.
- Use generics to write code against the protocol without erasing the concrete type.
Protocol-oriented design pairs cleanly with value types: a struct that conforms to a protocol composes without an inheritance hierarchy.
private by default
Every declaration starts at the most restrictive access level that still works. Open up only when a caller actually needs the symbol.
privatefor implementation details inside a type.fileprivatefor helpers shared across types in the same file.internal(the default) for module-level API. Spell it out anyway when reviewing diffs.publiconly on the surface other modules consume.openonly on classes designed for subclassing outside the module; almost never.
A loose access level today becomes someone else’s hard dependency tomorrow. For UI patterns, see swiftui.