---
title: "Swift: Best Practices"
slug: "swift"
category: "coding"
tags: ["coding", "swift", "concurrency", "ios", "value-types"]
status: "stable"
last_updated: 2026-05-14
summary: "Swift 6 defaults: structured concurrency, value types, optional handling, explicit errors, and private-by-default access."
related: ["[[coding/general-principles]]", "[[ios/swiftui]]", "[[ios/mapkit]]", "[[ios/core-data]]"]
---

## 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 [[coding/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.

```swift
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.

```swift
@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.

```swift
// Bad
let user = users.first!

// Better
guard let user = users.first else {
    return .empty
}
```

The exceptions are narrow: `IBOutlet`s 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."

```swift
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 [[coding/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 like `InvoiceStore`).
- 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.

- `private` for implementation details inside a type.
- `fileprivate` for helpers shared across types in the same file.
- `internal` (the default) for module-level API. Spell it out anyway when reviewing diffs.
- `public` only on the surface other modules consume.
- `open` only 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 [[ios/swiftui]].

## Related

- [[coding/general-principles]]
- [[ios/swiftui]]
- [[ios/mapkit]]
- [[ios/core-data]]
