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 letkeeps the happy path unindented and must exit the scope, so it is the default inside functions.if letis 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
| Semantics | Use it for | Watch out for |
|---|---|---|
struct | Models, view state, anything copied | Mutating a copy does not update the original |
final class | Identity, shared mutable state, delegates | Retain cycles through closures |
actor | State touched from several tasks | Every call becomes await |
enum | Closed sets of states and results | Exhaustive 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.Related
Layout, stacks and adaptive UI Architecture: MVVM, observation and dependency injection
Last refreshed 2026-09-18.