Swift Conventions
Swift 6 Language Mode
- Enable Swift 6 language mode to enforce data-race safety as compiler errors — not just warnings
- Migrate incrementally — enable
StrictConcurrency=complete per-target before flipping to Swift 6 mode project-wide
- Annotate types that cross isolation boundaries as
Sendable — the compiler rejects non-Sendable transfers between actors
- Use
@MainActor on UI-bound types and nonisolated to opt specific methods out of actor isolation
- Run SwiftFormat and SwiftLint on every build — enforce consistent style
- Fix all linter warnings before merging — treat warnings as errors in CI
- Follow Swift API Design Guidelines for naming conventions
- Use
swiftformat --lint in pre-commit hooks
Value Types & Immutability
- Prefer
let over var — use var only when the compiler requires mutation
- Use structs by default — use classes only for reference semantics or inheritance; structs are copied, eliminating shared mutation bugs
- Use enums with associated values for modeling finite state
- Prefer value semantics to avoid unintended shared mutation
Protocols
- Design small, focused protocols — one capability per protocol
- Use protocol extensions for default implementations
- Prefer composition of protocols over deep class hierarchies
- Use
some Protocol (opaque types) for return types to hide implementation
protocol Cacheable {
var cacheKey: String { get }
var ttl: TimeInterval { get }
}
extension Cacheable {
var ttl: TimeInterval { 300 } // 5 minutes default
}
struct UserProfile: Cacheable {
let id: String
let name: String
var cacheKey: String { "user-\(id)" }
}
Observation Framework (iOS 17+)
- Use
@Observable macro for state management in SwiftUI — replaces ObservableObject and @Published with simpler, more granular tracking
- SwiftUI views automatically track which
@Observable properties they read — no need for @ObservedObject wrappers
Noncopyable Types
- Use
~Copyable for types representing unique resources (file handles, locks, GPU buffers) — prevents accidental duplication
- Use
consuming and borrowing parameter modifiers to control ownership transfer
Macros
- Use Swift macros for compile-time code generation — prefer
@attached macros over runtime reflection
- Use
#Preview for SwiftUI previews, @Observable for observation, @Model for SwiftData
Concurrency
- Use structured concurrency with
async/await — no completion handlers; async/await prevents callback hell and race conditions
- Mark shared mutable state types as
Sendable — enable strict concurrency checking at compile time
- Use actors for shared mutable state protection — actors serialize access, eliminating data races
- Use
Task.detached sparingly — prefer structured task groups
actor CounterStore {
private var counts: [String: Int] = [:]
func increment(_ key: String) -> Int {
let newValue = (counts[key] ?? 0) + 1
counts[key] = newValue
return newValue
}
func value(for key: String) -> Int {
counts[key] ?? 0
}
}
Error Handling
- Use typed throws (Swift 6+) for domain-specific error types
- Define error enums conforming to
Error with descriptive cases
- Use
Result type for synchronous operations that can fail
- Handle errors at the appropriate level — do not over-catch
Testing
- Use Swift Testing framework with
@Test and #expect
- Group related tests with
@Suite for organization
- Use parameterized tests with
@Test(arguments:) for multiple inputs
- Mock dependencies using protocol conformances — not subclassing
Security & Secrets
- Store secrets in Keychain Services — never in UserDefaults or plists; UserDefaults is stored as plaintext XML
- Use App Transport Security — enforce HTTPS for all connections
- Validate all external input before processing
- Use
Data instead of String for sensitive values to control memory lifetime
SwiftData (iOS 17+)
- Use SwiftData with
@Model macro for persistence in new projects — simpler API than Core Data with SwiftUI integration
- Use
@Query in SwiftUI views for declarative data fetching with automatic UI updates
Server-Side Swift
- Use Vapor or Hummingbird for HTTP server applications — both support Swift 6 concurrency natively
- Share models and validation logic between client and server when both are Swift
Dependencies
- Use Swift Package Manager for dependency management
- Pin dependency versions with exact or range specifiers
- Audit transitive dependencies for security vulnerabilities
- Prefer platform frameworks over third-party libraries when equivalent