Swift essentials for iOS

Optionals and unwrapping, structs versus classes, protocols, closures and error handling — the language features every SwiftUI view depends on.

Optionals and the three ways out

struct Profile {
    let name: String
    var nickname: String?          // may genuinely be absent
}

func badge(for profile: Profile) -> String {
    // 1. guard: leave early when a value is required to continue
    guard let nickname = profile.nickname, !nickname.isEmpty else {
        return profile.name
    }
    // 2. optional chaining plus nil-coalescing for a fallback
    let initial = profile.nickname?.first.map(String.init) ?? "?"
    // 3. if let when the branch is small and local
    if let first = profile.name.first {
        return "\(nickname) (\(initial)) \(first)"
    }
    return nickname
}
  • guard let keeps the happy path unindented and must exit the scope, so it is the default inside functions.
  • if let is for a short branch where you keep going either way.
  • Avoid ! force unwrap in shipped code: a crash in the App Store is a rejection or a one-star review. Reserve it for tests and genuinely impossible states.
  • Use ?? only when a real fallback exists; inventing an empty string to silence the compiler hides a data problem.

Structs, classes and protocols

SemanticsUse it forWatch out for
structModels, view state, anything copiedMutating a copy does not update the original
final classIdentity, shared mutable state, delegatesRetain cycles through closures
actorState touched from several tasksEvery call becomes await
enumClosed sets of states and resultsExhaustive switches must cover new cases

SwiftUI views are structs precisely because value semantics make diffing cheap and predictable. Protocols add the polymorphism: define the smallest protocol the caller needs, then conform types to it.

protocol PriceFormatting {
    func string(from amount: Decimal, currency: String) -> String
}

struct SimpleFormatter: PriceFormatting {
    func string(from amount: Decimal, currency: String) -> String {
        "\(amount) \(currency)"
    }
}

// extend the protocol instead of a base class: every conforming type gets this
extension PriceFormatting {
    func shortString(from amount: Decimal) -> String { string(from: amount, currency: "GBP") }
}

Closures and errors

enum LoadError: LocalizedError {
    case offline
    case badStatus(Int)

    var errorDescription: String? {
        switch self {
        case .offline: return "You appear to be offline."
        case .badStatus(let code): return "The server returned \(code)."
        }
    }
}

func load(_ complete: @escaping (Result<[String], LoadError>) -> Void) {
    guard !isOffline else { return complete(.failure(.offline)) }
    complete(.success(["one", "two"]))
}

do {
    let items = try parse([String]())
    print(items)
} catch let error as LoadError {
    print(error.localizedDescription)
} catch {
    print("unexpected")
}
⚠️
A closure stored on a class property captures self strongly. Use [weak self] or [unowned self] in long-lived closures, otherwise the object is never deallocated and the leak only shows up in Instruments.

FAQ

Should I use classes or structs for view models?
Use a class annotated @Observable so SwiftUI can track mutations by reference. Use structs for the data the view model exposes — they copy cheaply and are safe to hand to other tasks.
What is the difference between <code>try?</code> and <code>try!</code>?
try? converts a thrown error into nil, so the failure disappears silently. try! crashes on failure. Prefer do-catch when you can act on the error, and try? only when the failure is genuinely uninteresting.

Layout, stacks and adaptive UI Architecture: MVVM, observation and dependency injection

Last refreshed 2026-09-18.