Skip to content
Back to skills

Swift Patterns

ASecurity

Swift/iOS: SwiftUI, Combine, async/await, actors, SPM, Core Data, UIKit interop. Triggers: Swift, SwiftUI, Combine, iOS, Xcode, actor, Core Data, @MainActor, @State.

  • 177 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added May 27, 2026
developmentgoswiftkotlinbashreactexpresstestinggitapidatabase

Works with

  • api

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned May 27, 2026

npx -y skills add softspark/ai-toolkit --skill swift-patterns --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Swift Patterns?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Swift Patterns
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/softspark-swift-patterns/badge)](https://www.skillsdirectory.com/skills/softspark-swift-patterns)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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

Files in this skill

  • SKILL.md12.4 KB
  • reference/frameworks.md2.9 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.