Chapter 01
Swift essentials
The Swift 6 that SwiftUI code relies on: optionals, value types, enums, closures, protocols, errors and structured concurrency with actors.
let, var and type inference
Also called type inference
- What
letdeclares a constant andvara variable. Swift infers the type from the initial value, and every value has a fixed type.- Why
- Prefer
let: the compiler warns when avaris never changed, and constants make value types safe to share.
let name = "Ana"
var score = 0
score += 10
let scores: [String: Int] = ["ana": 90, "li": 72]
let passed = scores.filter { $0.value >= 75 }.map(\.key)Optionals and unwrapping
Also called if let, guard let, optional, force unwrap, optional chaining
- What
- An optional
String?either holds a value ornil. You unwrap it safely withif let,guard let, optional chaining?.or a default with??. - Why
- Swift will not let you use a possibly missing value by accident, which removes a whole class of crashes.
var nickname: String? = nil
let shown = nickname ?? "Guest"
if let nickname {
print("Hi, \(nickname)")
}
func greet(_ name: String?) {
guard let name else { return }
print("Hello, \(name)")
}Watch out
Force unwrapping with ! crashes when the value is nil; reserve it for values that truly cannot be missing.
Structs versus classes
Also called value types, reference types, copy-on-write
- What
- Structs are value types: assigning one makes an independent copy. Classes are reference types: several variables can point at the same object.
- Why
- SwiftUI views and most models are structs, which makes state changes predictable. Use a class when you need shared identity, such as an
@Observablemodel many views read.
struct Point { var x = 0 }
var a = Point()
var b = a
b.x = 5
print(a.x) // 0: b is a copy
final class Box { var x = 0 }
let c = Box()
let d = c
d.x = 5
print(c.x) // 5: c and d share one objectEnums with associated values
Also called associated value
- What
- Swift enums can carry data with each case, such as
.loaded([Post]). Aswitchover an enum must cover every case. - Why
- Screen state, API results and navigation routes are modelled as enums, so the compiler stops you forgetting the error or empty case.
enum LoadState {
case loading
case loaded([Post])
case failed(any Error)
}
switch state {
case .loading: ProgressView()
case .loaded(let posts): PostList(posts: posts)
case .failed(let error): Text(error.localizedDescription)
}Closures and capture lists
Also called [weak self], trailing closure, capture list, escaping closure, retain cycle
- What
- Closures are functions without a name that capture surrounding values. A trailing closure goes after the parentheses. A capture list like
[weak self]controls how a class instance is captured. - Why
- SwiftUI actions, sorting and completion handlers are closures, and capture lists prevent retain cycles that leak memory.
let sorted = names.sorted { $0 < $1 }
final class ProfileModel {
var onSave: (() -> Void)?
func bind(to button: SaveButton) {
button.action = { [weak self] in
self?.save()
}
}
func save() {}
}Watch out
A class that stores a closure which strongly captures self never gets freed.
Protocols and extensions
Also called protocol, protocol extension
- What
- A protocol lists requirements a type promises to meet, like
IdentifiableorCodable. An extension adds methods, including default implementations, to an existing type or protocol. - Why
- SwiftUI is built on protocols (
View,Hashable), and protocols let you swap a real network client for a fake in tests.
protocol Shape {
var area: Double { get }
}
struct Circle: Shape {
let radius: Double
var area: Double { .pi * radius * radius }
}
extension Shape {
func describe() -> String { "Area \(area)" }
}Error handling with throws
Also called do try catch, typed throws
- What
- A function marked
throwscan fail; callers usetryinsidedoandcatch. Swift 6 also allows typed throws, such asthrows(LoginError), when the error type is fixed. - Why
- Network, decoding and file work all throw, and handling errors explicitly is what turns a failure into a useful message instead of a silent blank screen.
enum LoginError: Error { case badPassword, locked }
func login(_ pass: String) throws(LoginError) -> User {
guard pass.count >= 8 else { throw .badPassword }
return User()
}
do {
let user = try login(input)
show(user)
} catch {
showMessage(for: error)
}Watch out
Using try? everywhere throws the error away, so you never learn why something failed.
async/await and Task
Also called async let, structured concurrency, Swift concurrency
- What
- An
asyncfunction can suspend while waiting.awaitmarks each suspension point,async letruns work in parallel, andTaskstarts async work from synchronous code. - Why
- Networking and databases are async. In SwiftUI the
.taskmodifier starts work when a view appears and cancels it when the view goes away.
func loadDashboard() async throws -> Dashboard {
async let user = api.user()
async let feed = api.feed()
return try await Dashboard(user: user, feed: feed)
}
Task {
do { dashboard = try await loadDashboard() }
catch { errorMessage = error.localizedDescription }
}Actors, MainActor and Sendable
Also called data race safety, strict concurrency, main actor, data race, actor
- What
- An
actorprotects its state so only one task touches it at a time.@MainActorruns code on the main thread, andSendablemarks types safe to pass between tasks. Swift 6 checks all of this at compile time. - Why
- Data races cause random crashes that are nearly impossible to reproduce. Swift 6 language mode turns them into compile errors, and UI code must run on the main actor.
actor ImageCache {
private var images: [URL: Data] = [:]
func image(for url: URL) -> Data? { images[url] }
func store(_ data: Data, for url: URL) { images[url] = data }
}
@MainActor
@Observable
final class FeedModel {
var posts: [Post] = []
}Watch out
Silencing concurrency errors with @unchecked Sendable or nonisolated(unsafe) hides real races instead of fixing them.
Swift Package Manager
Also called SPM, SwiftPM
- What
- Apple's dependency manager, built into Xcode. You add a package by its Git URL and a version rule, and Xcode resolves and builds it with your app.
- Why
- It is the standard way to add libraries on iOS today, and splitting your own code into local packages speeds up builds and previews.
- Example
- In Xcode choose File, Add Package Dependencies, paste the repository URL, pick "Up to Next Major Version" and add the library to your app target. The resolved versions are saved in
Package.resolved, which you commit.
Watch out
Not committing Package.resolved, so teammates and CI build with different library versions.
Swift collections
Also called Array, Dictionary, Set, map, compactMap
- What
- Array, Dictionary and Set are value types: assigning one copies it, cheaply, with copy on write. Functions such as map, filter, reduce and compactMap transform them.
- Why
- Lists of models feed every SwiftUI screen, and value semantics mean a copied array never changes behind your back.
var titles = ["Buy milk", "Call Sara"]
var copy = titles
copy.append("Gym") // titles still has 2 items
let lengths = titles.map { $0.count }
let ids = ["1", "x", "3"].compactMap { Int($0) } // [1, 3]
var byId: [Int: String] = [:]
byId[1] = "Buy milk"
let done: Set<Int> = [1, 3]Watch out
Force-unwrapping a dictionary lookup with !, which crashes when the key is missing.