--- 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 { var wrappedValue: Value { didSet { wrappedValue = min(max(wrappedValue, range.lowerBound), range.upperBound) } } let range: ClosedRange init(wrappedValue: Value, _ range: ClosedRange) { 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 { 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