Swift Development (6+)
Modern Swift best practices for writing safe, expressive, and idiomatic code. Targets Swift 6+ — language and standard library only, no platform frameworks.
Applicable to all Swift targets: Apple platforms, server-side (Vapor, Hummingbird), CLI tools, cross-platform (Linux, Windows).
Reference Files
references/type-system.md— Generics, protocols with associated types, opaque types (some), existentials (any), metatypes,@dynamicMemberLookup,@dynamicCallablereferences/concurrency.md— Actors, task groups, async sequences, Sendable, isolation, GCD migration, Combine interopreferences/patterns.md— Property wrappers, result builders, key paths, Codable, extensions, copy-on-write, DSL designreferences/project-structure.md— Swift Package Manager setup, multi-target packages, build configurations, plugins, testing setupreferences/static-analysis.md— SwiftLint (configuration, rules, custom rules, auto-correct), SwiftFormat (configuration, formatting rules), combined setup, CI/CD integration, pre-commit hooks
Code Style
- Short functions — target under 20 lines. Extract well-named helpers when a block needs a comment.
- Imports at the top — group by module. No
@_exportedunless building a module facade. - Access control — default to
privateorinternal. Usepubliconly at module boundaries.
Naming Conventions
| Element | Convention | Example |
|---|---|---|
| Type / Protocol | PascalCase |
UserProfile, Fetchable |
| Function / Property | camelCase |
fetchUser(), isActive |
Constant (let at module scope) |
camelCase |
defaultTimeout |
| Enum case | camelCase |
case loading, case success(Data) |
| Boolean | Prefix is, has, can, should |
isValid, hasPermission |
| File name | PascalCase.swift |
UserProfile.swift |
| Protocol | Adjective or -able/-ible suffix |
Loadable, Configurable |
| Generic parameter | Descriptive or single uppercase | Element, T, Key, Value |
Follow the Swift API Design Guidelines:
- Name functions and methods according to their side effects: noun for non-mutating (
distance(to:)), verb for mutating (sort()),-ed/-ingfor non-mutating variant (sorted(),removing()). - Prefer clarity over brevity. A name should read naturally at the call site.
- Label closure parameters and tuple members.
Value Types vs Reference Types
struct User { // Value type — prefer by default
let id: UUID
var name: String
}
class UserStore { // Reference type — when identity matters
private var users: [User] = []
}
Use struct by default. Use class when you need identity, inheritance, or reference semantics. Actors when you need thread-safe mutable state.
Optionals
let name: String? = user?.name // safe chaining
let displayName = name ?? "Anonymous" // nil coalescing
// NEVER force-unwrap unless you have a provable invariant with a comment
guard let user = fetchUser(id) else { return } // early exit — preferred
Safe patterns
// Shorthand optional binding (Swift 5.7+) — use when binding name matches the variable
if let name {
print(name)
}
guard let user else { return }
// Explicit binding — only when intentionally binding to a different name
if let email = user.email {
sendConfirmation(to: email)
}
// map / flatMap
let uppercased = name.map { $0.uppercased() } // String?
let nested = fetchUser(id).flatMap { $0.address } // Address?
// Coalescing with throwing
let user = try optionalUser ?? { throw AppError.notFound }()
Always use shorthand optional binding when the binding name matches the original variable name. Write if let value { } instead of if let value = value { }. This applies to all optional binding contexts: if let, guard let, while let, and multi-condition bindings (e.g., guard let foo, let bar else { }). Only use the explicit if let renamed = original form when intentionally binding to a different name.
NEVER use ! to silence the compiler. Acceptable only with a provable invariant and a comment explaining why.
Enums with Associated Values
enum LoadingState<T> {
case idle
case loading
case loaded(T)
case failed(Error)
}
Prefer enums over boolean flags for state modeling. Enables exhaustive switch.
Protocols and Extensions
protocol Repository {
associatedtype Entity
func fetch(id: UUID) async throws -> Entity
func save(_ entity: Entity) async throws
}
extension Array where Element: Identifiable {
func element(withID id: Element.ID) -> Element? {
first { $0.id == id }
}
}
Rules:
- Prefer protocol composition (
Fetchable & Cacheable) over large monolithic protocols. - Use extensions to organize conformances — one extension per protocol conformance.
- Default implementations in protocol extensions for shared behavior.
For generics, opaque types, existentials, and advanced protocol patterns see references/type-system.md.
Error Handling
// Define domain errors
enum AppError: LocalizedError {
case networkUnavailable
case unauthorized
case notFound(resource: String)
var errorDescription: String? {
switch self {
case .networkUnavailable: "Network is unavailable"
case .unauthorized: "Authentication required"
case .notFound(let resource): "\(resource) not found"
}
}
}
// Use typed throws (Swift 6+)
func fetchUser(id: UUID) async throws(AppError) -> User { ... }
// Use Result for expected outcomes in callbacks
func validate(_ input: String) -> Result<ValidatedInput, ValidationError> { ... }
Rules:
- Use
guardfor preconditions with early exit. - Catch the narrowest error type. Never catch
Errorbroadly unless at a top-level boundary. - Prefer
async throwsoverResultfor async operations. - Use
LocalizedErrorfor user-facing error messages. - Typed throws (Swift 6+) for functions with a single known error type.
Concurrency Essentials
// async/await
func fetchUser(id: UUID) async throws -> User {
let data = try await networkClient.get("users/\(id)")
return try decode(data)
}
// Structured concurrency — parallel execution
func loadDashboard() async throws -> Dashboard {
async let profile = fetchProfile()
async let notifications = fetchNotifications()
return Dashboard(profile: try await profile, notifications: try await notifications)
}
// Actor — thread-safe mutable state
actor Cache<Key: Hashable, Value> {
private var storage: [Key: Value] = [:]
func get(_ key: Key) -> Value? { storage[key] }
func set(_ key: Key, value: Value) { storage[key] = value }
}
Rules:
- Use structured concurrency —
async let,TaskGroup. AvoidTask.detachedunless truly needed. - Never block the calling thread. Offload heavy work with
Taskor actors. - Sendable compliance — Swift 6 strict concurrency requires types crossing isolation boundaries to be
Sendable. - Use
Task.isCancelledortry Task.checkCancellation()to respond to cancellation. - Mark
nonisolatedexplicitly when actor methods don't need isolation.
For actors, task groups, async sequences, Sendable patterns, GCD migration, and Combine interop see references/concurrency.md.
Collections
// Prefer higher-order functions over manual loops
let activeUsers = users.filter { $0.isActive }
let names = users.map(\.name)
let totalAge = users.reduce(0) { $0 + $1.age }
// Lazy for large collections
let firstMatch = users.lazy.filter { $0.isActive }.first
// Dictionary grouping
let byDepartment = Dictionary(grouping: employees, by: \.department)
Closures
// Trailing closure syntax
let sorted = users.sorted { $0.name < $1.name }
// Multi-line closures — use named parameters
let transformed = items.map { item in
Item(
id: item.id,
name: item.name.uppercased()
)
}
// @escaping — when closure outlives the function
func fetch(completion: @escaping (Result<User, Error>) -> Void) { ... }
// @Sendable — when closure crosses concurrency boundaries
func execute(_ work: @Sendable () async -> Void) { ... }
Rules:
- Use
$0,$1only for short, single-expression closures. - Name parameters explicitly when the closure body is multi-line.
- Prefer
asyncfunctions over completion handler closures in new code.
Project Structure
MyPackage/
├── Package.swift
├── Sources/
│ ├── MyLibrary/
│ │ ├── Models/
│ │ ├── Services/
│ │ └── Extensions/
│ └── MyExecutable/
│ └── main.swift
├── Tests/
│ └── MyLibraryTests/
└── Plugins/
Rules:
- Use Swift Package Manager for dependency management.
- Organize by domain, not by technical role.
- One type per file, file name matches type name.
- Separate targets for libraries and executables.
For SPM setup, multi-target packages, build configurations, plugins, and testing see references/project-structure.md.
Testable Design
- Dependency injection — inject protocols, not concrete types. Use init injection.
- Pure logic — keep business logic free of framework and I/O dependencies.
- Fakes over mocks — write simple in-memory protocol conformances.
- Test naming —
test_methodName_whenCondition_expectedResult.
protocol UserRepository {
func fetch(id: UUID) async throws -> User
}
// Production
struct RemoteUserRepository: UserRepository {
let client: HTTPClient
func fetch(id: UUID) async throws -> User { ... }
}
// Test
struct FakeUserRepository: UserRepository {
var stubbedUser: User?
func fetch(id: UUID) async throws -> User {
guard let user = stubbedUser else { throw AppError.notFound(resource: "User") }
return user
}
}
Swift Testing (Swift 6+)
import Testing
@Test("Fetches user by ID")
func fetchUser() async throws {
let repo = FakeUserRepository(stubbedUser: User(id: testID, name: "Alice"))
let user = try await repo.fetch(id: testID)
#expect(user.name == "Alice")
}
@Test("Throws when user not found")
func fetchUserNotFound() async {
let repo = FakeUserRepository(stubbedUser: nil)
await #expect(throws: AppError.self) {
try await repo.fetch(id: testID)
}
}
Prefer Swift Testing (@Test, #expect) over XCTest for new code. Use XCTest when the project requires it or for UI/integration tests on Apple platforms.
Quick Reference: Common Mistakes
| Mistake | Fix |
|---|---|
Force-unwrapping (!) without invariant |
Use guard let, if let, ??, or optional chaining |
if let value = value { } when names match |
Use shorthand: if let value { } (Swift 5.7+) |
Catching Error broadly |
Catch specific error types at appropriate boundaries |
Task { } without cancellation handling |
Check Task.isCancelled or use Task.checkCancellation() |
Using GCD (DispatchQueue) in new code |
Use Swift Concurrency (async/await, actors) |
Mutable var in struct properties without need |
Default to let; use var only when mutation is required |
| Large protocols (>5 requirements) | Break into focused protocols; use composition |
Ignoring Sendable warnings |
Conform value types or use actor isolation |
| Completion handlers in new code | Use async/await; wrap legacy APIs with withCheckedContinuation |
| Stringly-typed APIs | Use enums, phantom types, or value types for type safety |
Any / AnyObject without reason |
Use generics or existentials with constraints |
Double for money |
Use Decimal with explicit rounding |
| Hard-coded dependencies | Protocol-based DI via init injection |
#if directives for multi-target polymorphism |
Use DI or separate file implementations per target; reserve #if for DEBUG, os(), canImport() |