---
name: swift-patterns
description: "Swift/iOS: SwiftUI, Combine, async/await, actors, SPM, Core Data, UIKit interop. Triggers: Swift, SwiftUI, Combine, iOS, Xcode, actor, Core Data, @MainActor, @State."
effort: medium
user-invocable: false
allowed-tools: Read
---

# Swift / iOS Patterns

## Project Structure

### Swift Package (SPM)

```
MyPackage/
├── Package.swift
├── Sources/
│   ├── MyLibrary/
│   └── MyExecutable/
├── Tests/
│   └── MyLibraryTests/
└── Plugins/
```

```swift
// swift-tools-version: 5.10
import PackageDescription

let package = Package(
    name: "MyPackage",
    platforms: [.iOS(.v17), .macOS(.v14)],
    products: [
        .library(name: "MyLibrary", targets: ["MyLibrary"]),
    ],
    dependencies: [
        .package(url: "https://github.com/apple/swift-algorithms", from: "1.2.0"),
    ],
    targets: [
        .target(name: "MyLibrary",
                dependencies: [.product(name: "Algorithms", package: "swift-algorithms")]),
        .testTarget(name: "MyLibraryTests", dependencies: ["MyLibrary"]),
    ]
)
```

### Xcode Project Layout

```
MyApp/
├── MyApp/
│   ├── App/             # Entry point, ContentView
│   ├── Features/        # Feature modules (Views, ViewModels, Models)
│   ├── Core/            # Networking, Storage, Extensions
│   └── Resources/       # Assets.xcassets, Info.plist
├── MyAppTests/
└── MyAppUITests/
```

---

## Idioms / Code Style

### Optionals

```swift
// guard-let for early exit
func process(user: User?) {
    guard let user else { return }
    print(user.name)
}

// Optional chaining + nil coalescing
let name = user?.profile?.displayName ?? "Anonymous"

// map/flatMap on optionals
let length: Int? = optionalString.map { $0.count }
// Never force-unwrap in production: user!.name
```

### Protocol-Oriented Programming

```swift
protocol Cacheable: Identifiable where ID: Hashable {
    var cacheKey: String { get }
}

extension Cacheable where ID == String {
    var cacheKey: String { id }
}
```

### Value Types vs Reference Types

Use structs by default (value semantics, thread-safe). Use classes when identity matters, shared mutable state is intentional, inheritance is needed, or ObjC interop is required.

### Property Wrappers

```swift
@propertyWrapper
struct Clamped<Value: Comparable> {
    var wrappedValue: Value {
        didSet { wrappedValue = min(max(wrappedValue, range.lowerBound), range.upperBound) }
    }
    let range: ClosedRange<Value>

    init(wrappedValue: Value, _ range: ClosedRange<Value>) {
        self.range = range
        self.wrappedValue = min(max(wrappedValue, range.lowerBound), range.upperBound)
    }
}

struct Volume {
    @Clamped(0...100) var level: Int = 50
}
```

### Result Builders

```swift
@resultBuilder
struct ArrayBuilder<Element> {
    static func buildBlock(_ components: [Element]...) -> [Element] { components.flatMap { $0 } }
    static func buildExpression(_ expression: Element) -> [Element] { [expression] }
    static func buildOptional(_ component: [Element]?) -> [Element] { component ?? [] }
}
```

---

## Error Handling

### throws / try / catch

```swift
enum NetworkError: Error, LocalizedError {
    case invalidURL
    case timeout(seconds: Int)
    case serverError(statusCode: Int)

    var errorDescription: String? {
        switch self {
        case .invalidURL: "Invalid URL."
        case .timeout(let s): "Timed out after \(s)s."
        case .serverError(let code): "Server returned \(code)."
        }
    }
}

do {
    let user = try fetchUser(id: "123")
} catch let error as NetworkError {
    handleNetworkError(error)
} catch {
    handleUnexpected(error)
}

let user = try? fetchUser(id: "123") // nil on error
```

### Result Type

```swift
switch fetchData(from: url) {
case .success(let data): process(data)
case .failure(let error): showError(error)
}
```

### Typed Throws (Swift 6) and Async Throws

```swift
func load() throws(DatabaseError) -> [Item] { /* compiler-enforced error type */ }

func fetchUser(id: String) async throws -> User {
    let (data, response) = try await URLSession.shared.data(from: url)
    guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {
        throw NetworkError.serverError(statusCode: 0)
    }
    return try JSONDecoder().decode(User.self, from: data)
}
```

---

## Testing

### XCTest

```swift
final class UserServiceTests: XCTestCase {
    var sut: UserService!
    var mockRepo: MockUserRepository!

    override func setUp() {
        mockRepo = MockUserRepository()
        sut = UserService(repository: mockRepo)
    }

    func testFetchUser_success() async throws {
        mockRepo.stubbedUser = User(id: "1", name: "Alice")
        let user = try await sut.fetchUser(id: "1")
        XCTAssertEqual(user.name, "Alice")
    }

    func testFetchUser_notFound_throws() async {
        mockRepo.shouldFail = true
        do {
            _ = try await sut.fetchUser(id: "999")
            XCTFail("Expected error")
        } catch {
            XCTAssertTrue(error is UserService.Error)
        }
    }
}
```

### Swift Testing Framework (Swift 6+)

```swift
import Testing

@Suite("UserService")
struct UserServiceTests {
    @Test("fetches user by ID")
    func fetchUser() async throws {
        let mockRepo = MockUserRepository()
        mockRepo.stubbedUser = User(id: "1", name: "Alice")
        let sut = UserService(repository: mockRepo)
        let user = try await sut.fetchUser(id: "1")
        #expect(user.name == "Alice")
    }

    @Test("throws on missing user", arguments: ["999", ""])
    func fetchMissingUser(id: String) async {
        let sut = UserService(repository: MockUserRepository())
        await #expect(throws: UserService.Error.self) {
            try await sut.fetchUser(id: id)
        }
    }
}
```

### Protocol-Based Mocking

```swift
protocol UserRepository {
    func fetch(id: String) async throws -> User
}

final class MockUserRepository: UserRepository {
    var stubbedUser: User?
    var shouldFail = false
    private(set) var fetchCallCount = 0

    func fetch(id: String) async throws -> User {
        fetchCallCount += 1
        if shouldFail { throw NSError(domain: "", code: 0) }
        return stubbedUser ?? User(id: id, name: "Default")
    }
}
```

### UI Testing

```swift
func testLoginFlow() {
    let app = XCUIApplication()
    app.launchArguments = ["--uitesting"]
    app.launch()
    app.textFields["email"].tap()
    app.textFields["email"].typeText("user@example.com")
    app.secureTextFields["password"].typeText("pass")
    app.buttons["Sign In"].tap()
    XCTAssertTrue(app.staticTexts["Welcome"].waitForExistence(timeout: 5))
}
```

---

## Common Frameworks

For SwiftUI + `@Observable`, Combine, Structured Concurrency, SwiftData, and Vapor framework patterns with complete code examples, see [reference/frameworks.md](reference/frameworks.md).

---

## Performance

### Copy-on-Write

Array, String, Dictionary use COW automatically. For custom value types:

```swift
struct LargeData {
    private final class Storage { var buffer: [UInt8]; init(_ b: [UInt8]) { buffer = b } }
    private var storage: Storage

    var buffer: [UInt8] {
        get { storage.buffer }
        set {
            if !isKnownUniquelyReferenced(&storage) { storage = Storage(newValue) }
            else { storage.buffer = newValue }
        }
    }
}
```

### ARC Retain Cycles

```swift
// weak — closure may outlive self
service.fetch { [weak self] result in
    guard let self else { return }
    self.update(with: result)
}

// unowned — self guaranteed to outlive closure
lazy var tick: () -> Void = { [unowned self] in self.count += 1 }
```

### Sendable and Actors

```swift
struct Config: Sendable { let apiURL: URL; let timeout: TimeInterval }

actor ImageCache {
    private var cache: [URL: Data] = [:]
    func image(for url: URL) -> Data? { cache[url] }
    func store(_ data: Data, for url: URL) { cache[url] = data }
}
```

### Instruments

| Instrument | Use For |
|---|---|
| Time Profiler | CPU bottlenecks |
| Allocations | Memory growth |
| Leaks | Retain cycles |
| SwiftUI | View body re-evaluations |
| Core Animation | FPS, offscreen rendering |

Profile on device (not simulator). Use `os_signpost` for custom spans.

---

## Build / Package Management

### SPM Commands

```bash
swift package resolve       # Resolve deps
swift build -c release      # Release build
swift test --filter MyTests # Filtered test run
```

### xcconfig

```
// Shared.xcconfig
SWIFT_VERSION = 5.10
IPHONEOS_DEPLOYMENT_TARGET = 17.0
SWIFT_STRICT_CONCURRENCY = complete

// Debug.xcconfig
#include "Shared.xcconfig"
SWIFT_OPTIMIZATION_LEVEL = -Onone
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG

// Release.xcconfig
#include "Shared.xcconfig"
SWIFT_OPTIMIZATION_LEVEL = -O
SWIFT_COMPILATION_MODE = wholemodule
```

### Tuist

Define targets in `Project.swift` using `ProjectDescription`. Map each app, framework, and test target with `bundleId`, `sources`, and `dependencies`. Use `.external(name:)` for SPM deps and `.target(name:)` for internal.

Schemes: separate Debug/Release/Testing. Enable ASan + TSan in test schemes.

---

## Anti-Patterns
| Anti-Pattern | Problem | Fix |
|---|---|---|
| Force unwrap `!` | Runtime crash | `guard let`, `if let`, `??` |
| Massive view controller | Untestable | MVVM, composable views |
| Stringly-typed APIs | No compiler checks | Enums, phantom types |
| Ignoring `@MainActor` | Off-main-thread UI | Annotate view models |
| Retain cycles | Memory leaks | `[weak self]` / `[unowned self]` |
| Blocking main thread | UI freezes | `async/await`, `Task { }` |
| `UserDefaults` for secrets | Insecure | Keychain (`SecItemAdd`) |
| `@ObservedObject` for owned state | Object recreated | `@StateObject` or `@State` + `@Observable` |

## Rules

- **MUST** use Swift concurrency primitives (`async/await`, actors, `Task`) for new code — GCD is legacy and mixes poorly with the new model
- **MUST** annotate view models with `@MainActor` when they touch UI state — off-main mutations cause runtime warnings and flaky UI
- **NEVER** force-unwrap (`!`) without a documented invariant in a comment; runtime crashes from unwrap are the top iOS crash category
- **NEVER** store secrets in `UserDefaults` or plist — use Keychain APIs (`SecItemAdd`, `SecItemCopyMatching`)
- **CRITICAL**: SwiftUI state flows downward; mutations flow through `@State`, `@Binding`, or `@Observable`. Never mutate a parent's state from a child via a captured reference — it breaks dependency tracking.
- **MANDATORY**: every closure that captures `self` inside a reference type uses `[weak self]` or `[unowned self]` — retain cycles are the top memory-leak cause

## Gotchas

- `@StateObject` and `@ObservedObject` look similar but behave oppositely on parent re-render: `@StateObject` persists, `@ObservedObject` may re-initialize. Using `@ObservedObject` for view-owned state recreates the object on every render — state loss without error.
- `Task { @MainActor in ... }` inside a non-`@MainActor` context does **not** synchronously return to main; it schedules. Code between the `await` and `Task` boundary runs on whatever actor you came from, which can race with UI updates.
- `AsyncStream` continuations without `onTermination` leak: if the consumer cancels, the producer keeps yielding forever. Always install a termination handler.
- SwiftData `@Query` with `@Environment(\.modelContext)` invalidates on every write; heavy reads in a watched view cause perf drops. Use `@FetchRequest`-style fetch descriptors with explicit refresh, not ambient `@Query`, for large datasets.
- Combine's `.receive(on: DispatchQueue.main)` schedules asynchronously — if the next operator expects sync execution, order matters. Prefer moving `.receive(on:)` to just before the sink, not mid-pipeline.
- Swift Concurrency does not compose cleanly with Objective-C completion handlers; `withCheckedContinuation` bridges but a continuation that is never resumed hangs the Task forever. Always pair resumes with all control-flow paths, including errors.

## When NOT to Load

- For **Flutter or React Native** cross-platform code — use `/flutter-patterns` or JS patterns; this skill is Swift-only
- For generic iOS architecture decisions (MVC vs MVVM vs VIPER) — use `/architecture-decision`
- For Kotlin-based cross-platform mobile (KMP) — use `/kotlin-patterns`
- For mobile CI/CD specifics (TestFlight, Fastlane) — use `/ci-cd-patterns`
- For Objective-C interop deep dives — outside scope; this skill focuses on modern Swift
