| name | swift-patterns |
| description | Core Swift patterns — value vs reference types, protocols, generics, optionals, Result, error handling, Codable, and module organization. Foundation for all Swift development. |
Swift Patterns
Core language patterns for production Swift (5.9+).
When to Activate
- Writing Swift types and business logic
- Designing protocols and generics
- Handling errors and optionals safely
- Serializing/deserializing data with Codable
- Reviewing Swift code for force unwraps, bare
catch blocks, or public mutable state that should be private
- Choosing between
struct and class for a new model or service type
- Deciding between existentials (
any Protocol) and generics (<T: Protocol>) for a function signature
Value Types vs Reference Types
Swift's type system distinguishes between value types (copied) and reference types (shared):
struct Point {
var x: Double
var y: Double
func distance(to other: Point) -> Double {
sqrt(pow(x - other.x, 2) + pow(y - other.y, 2))
}
}
enum Direction {
case north, south, east, west
var opposite: Direction {
switch self {
case .north: return .south
case .south: return .north
case .east: return .west
case .west: return .east
}
}
}
final class UserSession {
private(set) var currentUser: User?
private(set) var isAuthenticated = false
func login(user: User) {
currentUser = user
isAuthenticated = true
}
}
Protocols
Defining and Conforming
protocol Repository {
associatedtype Entity
associatedtype ID
func findById(_ id: ID) async throws -> Entity?
func save(_ entity: Entity) async throws -> Entity
func delete(id: ID) async throws
}
protocol Loggable {
var logDescription: String { get }
}
extension Loggable {
func log() {
print("[\(type(of: self))] \(logDescription)")
}
}
struct User: Loggable {
let id: UUID
var name: String
var email:
logDescription: {
}
}
Protocol Composition
typealias Identifiable = Hashable & CustomStringConvertible
protocol Persistable: Codable, Identifiable {}
func logAll(_ items: [any Loggable]) {
items.forEach { $0.log() }
}
Existentials vs Generics
func processAll<T: Persistable>(_ items: [T]) {
for item in items { save(item) }
}
func processHeterogeneous(_ items: [any Persistable]) {
for item in items { save(item) }
}
func makeValidator() -> some Validator {
EmailValidator()
}
Generics
func first<T>(_ array: [T], where predicate: (T) -> Bool) -> T? {
array.first(where: predicate)
}
struct Stack<Element> {
private var storage: [Element] = []
mutating func push(_ element: Element) {
storage.append(element)
}
mutating func pop() -> Element? {
storage.popLast()
}
var top: Element? { storage.last }
var isEmpty: Bool { storage.isEmpty }
}
extension Stack where Element: Equatable {
func contains(_ element: Element) -> Bool {
storage.contains(element)
}
}
func merge<T: Hashable & Comparable>( : <>, : <>) -> [] {
a.union(b).sorted()
}
Optionals
if let user = findUser(id: userId) {
greet(user)
}
func processOrder(userId: String) throws {
guard let user = findUser(id: userId) else {
throw AppError.userNotFound(userId)
}
guard user.isActive else {
throw AppError.accountInactive
}
}
let displayName = user.nickname ?? user.name
let city = user.address?.city?.uppercased()
let validEmails = users.compactMap { $0.email }
let uppercased: String? = optionalString.map { $0.uppercased() }
Error Handling
enum NetworkError: Error {
case noConnection
case timeout(after: TimeInterval)
case httpError(statusCode: Int, body: Data?)
case decodingFailed(reason: String)
}
func fetchUser(id: UUID) async throws -> User {
let url = URL(string: "/api/users/\(id)")!
let (data, response) = try await URLSession.shared.data(from: url)
guard let http = response as? HTTPURLResponse else {
throw NetworkError.noConnection
}
guard http.statusCode == 200 else {
throw NetworkError.httpError(statusCode: http.statusCode, body: data)
}
do {
return try JSONDecoder().decode(User.self, from: data)
} catch {
.decodingFailed(reason: error.localizedDescription)
}
}
(: ) {
{
user fetchUser(id: id)
display(user)
} .noConnection {
showOfflineMessage()
} .httpError( code, ) code {
showNotFound()
} {
showGenericError(error)
}
}
Result Type
Use Result<Success, Failure> for synchronous operations where error is expected:
func parseEmail(_ raw: String) -> Result<Email, ValidationError> {
guard raw.contains("@") else {
return .failure(.invalidFormat("Missing @ symbol"))
}
guard !raw.hasPrefix("@") else {
return .failure(.invalidFormat("Missing local part"))
}
return .success(Email(raw: raw))
}
let result = parseEmail(input)
.map { email in email.normalized }
.mapError { err in UserFacingError(err) }
switch result {
case .success(let email): sendWelcome(to: email)
case .failure(let error): showError(error)
}
let email = try parseEmail(input).get()
Codable
struct Product: Codable {
let id: UUID
let name: String
let price: Decimal
let isAvailable: Bool
}
struct APIUser: Codable {
let id: Int
let fullName: String
let emailAddress: String
enum CodingKeys: String, CodingKey {
case id
case fullName = "full_name"
case emailAddress = "email"
}
}
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
decoder.keyDecodingStrategy = .convertFromSnakeCase
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
encoder.keyEncodingStrategy = .convertToSnakeCase
let user = try decoder.decode(., from: data)
json encoder.encode(user)
Module Organization
Sources/
MyApp/
Domain/ # Pure types, no frameworks
User.swift
Order.swift
Errors.swift
Application/ # Use cases — depends only on Domain
UserService.swift
OrderService.swift
Infrastructure/ # Adapters (URLSession, CoreData, etc.)
NetworkClient.swift
UserRepository.swift
Presentation/ # ViewModels, UI adapters
UserViewModel.swift
Tests/
MyAppTests/
Domain/
UserTests.swift
Application/
UserServiceTests.swift
Access Control
public struct UserService {
private let repository: any UserRepository
private let mailer: any Mailer
public init(repository: any UserRepository, mailer: any Mailer) {
self.repository = repository
self.mailer = mailer
}
public func register(name: String, email: String) async throws -> User { ... }
func validateEmail(_ email: String) throws { ... }
}
public struct Order {
public private(set) var status: .pending
() {
status .pending { .invalidTransition }
status .confirmed
}
}
Quick Reference
| Feature | Use case |
|---|
struct | Data, models, DTOs — value semantics |
class | Shared state, identity, ObjC interop |
final class | Class that won't be subclassed (preferred) |
protocol | Define behavior contracts |
some Protocol | Opaque return type (hides concrete type) |
any Protocol | Existential (heterogeneous collections) |
guard let | Early exit on nil/error |
?? | Nil coalescing default |
Result<T, E> | Sync operations with expected errors |
throws | Propagate unexpected errors |
Codable | JSON encode/decode |
private(set) | Read-only outside, writable inside |
Anti-Patterns
Force Unwrapping Optionals in Production Code
Wrong:
func displayUser(id: String) {
let user = findUser(id: id)!
label.text = user.name
}
Correct:
func displayUser(id: String) {
guard let user = findUser(id: id) else {
showError("User not found")
return
}
label.text = user.name
}
Why: Force unwrapping silently propagates assumptions; guard let makes the nil case explicit and recoverable.
Using Class When Struct Suffices
Wrong:
class Point {
var x: Double
var y: Double
init(x: Double, y: Double) { self.x = x; self.y = y }
}
Correct:
struct Point {
var x: Double
var y: Double
}
Why: Classes carry reference semantics and shared-mutability risks; use structs for data without identity.
Catching All Errors with a Bare catch
Wrong:
func loadProfile() async {
do {
let profile = try await fetchProfile()
display(profile)
} catch {
showGenericError()
}
}
Correct:
func loadProfile() async {
do {
let profile = try await fetchProfile()
display(profile)
} catch NetworkError.noConnection {
showOfflineBanner()
} catch NetworkError.httpError(let code, _) where code == 401 {
redirectToLogin()
} catch {
showGenericError(error)
}
}
Why: A bare catch collapses typed error information and makes distinct failure modes indistinguishable to callers.
Existential any Protocol Where Generic <T: Protocol> Is Better
Wrong:
func processItems(_ items: [any Persistable]) {
for item in items { save(item) }
}
Correct:
func processItems<T: Persistable>(_ items: [T]) {
for item in items { save(item) }
}
Why: Existentials (any) incur heap allocation and dynamic dispatch; prefer generics when the concrete type is uniform at the call site.
Exposing Mutable State Publicly
Wrong:
public struct Order {
public var status: OrderStatus = .pending
}
Correct:
public struct Order {
public private(set) var status: OrderStatus = .pending
public mutating func confirm() throws {
guard status == .pending else { throw OrderError.invalidTransition }
status = .confirmed
}
}
Why: Public mutable properties let callers bypass business rules; private(set) with mutating methods enforces invariants at compile time.
For advanced Swift — property wrappers, result builders, Combine, opaque/existential types, advanced protocol patterns, and performance optimization — see skill: swift-patterns-advanced.