swift-conventions

Swift Conventions — Expert Decisions

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "swift-conventions" with this command: npx skills add kaakati/rails-enterprise-dev/kaakati-rails-enterprise-dev-swift-conventions

Swift Conventions — Expert Decisions

Expert decision frameworks for Swift choices that require experience. Claude knows Swift syntax — this skill provides the judgment calls.

Decision Trees

Struct vs Class

Need shared mutable state across app? ├─ YES → Class (singleton pattern, session managers) └─ NO └─ Need inheritance hierarchy? ├─ YES → Class (UIKit subclasses, NSObject interop) └─ NO └─ Data model or value type? ├─ YES → Struct (User, Configuration, Point) └─ NO → Consider what identity means ├─ Same instance matters → Class └─ Same values matters → Struct

The non-obvious trade-off: Structs with reference-type properties (arrays, classes inside) lose copy-on-write benefits. A struct containing [UIImage] copies the array reference, not images — mutations affect all "copies."

async/await vs Combine vs Callbacks

Is this a one-shot operation? (fetch user, save file) ├─ YES → async/await (cleaner, better stack traces) └─ NO → Is it a stream of values over time? ├─ YES │ └─ Need transformations/combining? │ ├─ Heavy transforms → Combine (map, filter, merge) │ └─ Simple iteration → AsyncStream └─ NO → Must support iOS 14? ├─ YES → Combine or callbacks └─ NO → async/await with continuation

When Combine still wins: Multiple publishers needing combineLatest , merge , or debounce . Converting this to pure async/await requires manual coordination that Combine handles elegantly.

@MainActor Placement

Is every public method UI-related? ├─ YES → @MainActor on class/struct └─ NO └─ Does it manage UI state? (@Published, bindings) ├─ YES → @MainActor on class, nonisolated for non-UI methods └─ NO └─ Only some methods touch UI? ├─ YES → @MainActor on specific methods └─ NO → No @MainActor needed

Critical: @Published properties MUST be updated on MainActor. SwiftUI observes on main thread — background updates cause undefined behavior, not just warnings.

TaskGroup vs async let

Number of concurrent operations known at compile time? ├─ YES (2-5 fixed operations) → async let │ Example: async let user = fetchUser() │ async let posts = fetchPosts() │ └─ NO (dynamic count, array of IDs) → TaskGroup Example: for id in userIds { group.addTask { ... } }

async let gotcha: All async let values MUST be awaited before scope ends. Forgetting to await silently cancels the task — no error, just missing data.

NEVER Do

Memory & Retain Cycles

NEVER capture self strongly in stored closures:

// ❌ Retain cycle — ViewModel never deallocates class ViewModel { var onUpdate: (() -> Void)?

func setup() {
    onUpdate = { self.refresh() } // self → onUpdate → self
}

}

// ✅ Break with weak capture onUpdate = { [weak self] in self?.refresh() }

NEVER use unowned unless you can PROVE the reference outlives the closure. When in doubt, use weak . The crash from dangling unowned is worse than the nil-check cost.

NEVER forget Timer invalidation:

// ❌ Timer retains target — object never deallocates timer = Timer.scheduledTimer(target: self, selector: #selector(tick), ...)

// ✅ Block-based with weak capture + invalidate in deinit timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in self?.tick() } deinit { timer?.invalidate() }

Concurrency

NEVER access @Published from background:

// ❌ Undefined behavior — may work sometimes, crash others Task.detached { viewModel.isLoading = false // Background thread! }

// ✅ Explicit MainActor Task { @MainActor in viewModel.isLoading = false }

NEVER use Task { } for fire-and-forget without understanding cancellation:

// ❌ Task inherits actor context — may block UI func buttonTapped() { Task { await heavyOperation() } // Runs on MainActor! }

// ✅ Explicit detachment for background work func buttonTapped() { Task.detached(priority: .userInitiated) { await heavyOperation() } }

NEVER assume Task.cancel() stops execution immediately. Cancellation is cooperative — your code must check Task.isCancelled or use try Task.checkCancellation() .

Optionals

NEVER force-unwrap in production code except:

  • @IBOutlet — set by Interface Builder

  • URL(string: "https://known-valid.com")! — compile-time known strings

  • fatalError paths where crash is correct behavior

NEVER use implicitly unwrapped optionals (var user: User! ) for regular properties. Only valid for:

  • @IBOutlet connections

  • Two-phase initialization where value is set immediately after init

Protocol Design

NEVER make protocols require AnyObject unless you need weak references:

// ❌ Unnecessarily restricts to classes protocol DataProvider: AnyObject { func fetchData() -> Data }

// ✅ Only require AnyObject for delegates that need weak reference protocol ViewModelDelegate: AnyObject { // Needed for weak var delegate func viewModelDidUpdate() }

NEVER add default implementations that change protocol semantics:

// ❌ Dangerous — conformers might not override protocol Validator { func validate() -> Bool } extension Validator { func validate() -> Bool { true } // Silent "always valid" }

// ✅ Make requirement obvious or use different name extension Validator { func isAlwaysValid() -> Bool { true } // Clear this is a default }

iOS-Specific Patterns

Dependency Injection in ViewModels

// ✅ Protocol-based for testability protocol UserServiceProtocol { func fetchUser(id: String) async throws -> User }

@MainActor final class UserViewModel: ObservableObject { @Published private(set) var user: User? @Published private(set) var error: Error?

private let userService: UserServiceProtocol

init(userService: UserServiceProtocol = UserService()) {
    self.userService = userService
}

}

Why default parameter: Production code uses real service, tests inject mock. No container framework needed for most apps.

Property Wrapper Selection

Wrapper Use When Memory Behavior

@State

View-local primitive/value types View-owned, recreated on parent rebuild

@StateObject

View creates and owns the ObservableObject Created once, survives view rebuilds

@ObservedObject

View receives ObservableObject from parent Not owned, may be recreated

@EnvironmentObject

Shared across view hierarchy Must be injected by ancestor

@Binding

Two-way connection to parent's state Reference to parent's storage

The StateObject vs ObservedObject trap: Using @ObservedObject for a locally-created object causes recreation on every view update — losing all state.

Error Handling Strategy

// Domain-specific errors with recovery info enum UserError: LocalizedError { case notFound(userId: String) case unauthorized case networkFailure(underlying: Error)

var errorDescription: String? {
    switch self {
    case .notFound(let id): return "User \(id) not found"
    case .unauthorized: return "Please log in again"
    case .networkFailure: return "Connection failed"
    }
}

var recoverySuggestion: String? {
    switch self {
    case .notFound: return "Check the user ID and try again"
    case .unauthorized: return "Your session expired"
    case .networkFailure: return "Check your internet connection"
    }
}

}

Performance Traps

Copy-on-Write Gotchas

// ✅ COW works — array copied only on mutation var a = [1, 2, 3] var b = a // No copy yet b.append(4) // Now b gets its own copy

// ❌ COW broken — class inside struct struct Container { var items: NSMutableArray // Reference type! } var c1 = Container(items: NSMutableArray()) var c2 = c1 // Both point to same NSMutableArray c2.items.add(1) // Mutates c1.items too!

Lazy vs Computed

// lazy: Computed ONCE, stored lazy var dateFormatter: DateFormatter = { let f = DateFormatter() f.dateStyle = .medium return f }()

// computed: Computed EVERY access var formattedDate: String { dateFormatter.string(from: date) // Cheap, uses cached formatter }

Rule: Expensive object creation → lazy . Simple derived values → computed.

String Performance

// ❌ O(n) for each concatenation in loop var result = "" for item in items { result += item.description // Creates new String each time }

// ✅ O(n) total var result = "" result.reserveCapacity(estimatedLength) for item in items { result.append(item.description) }

// ✅ Best for joining let result = items.map(.description).joined(separator: ", ")

Quick Reference

Access Control Decision

Level Use When

private

Implementation detail within declaration

fileprivate

Shared between types in same file (rare)

internal

Module-internal, app code default

package

Same package, different module (Swift 5.9+)

public

Framework API, readable outside module

open

Framework API, subclassable outside module

Default to most restrictive. Start private , widen only when needed.

Naming Quick Check

  • Types: PascalCase nouns — UserViewModel , NetworkError

  • Protocols: PascalCase — capability (-able/-ible ) or description

  • Functions: camelCase verbs — fetchUser() , configure(with:)

  • Booleans: is/has/should/can prefix — isLoading , hasContent

  • Factory methods: make prefix — makeUserViewModel()

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

flutter conventions & best practices

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

getx state management patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

ruby oop patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

rails localization (i18n) - english & arabic

No summary provided by upstream source.

Repository SourceNeeds Review