Swift is a big language and you do not need most of it to start. This lesson is the subset a
SwiftUI app leans on every day, in the order it stops being optional — and every example is a real
type from the ordering app, not a Person struct with a name.
If you are coming from TypeScript, Kotlin or modern Java, most of this will read as familiar with different punctuation. Two things will not: value semantics, and how thoroughly optionals are enforced. Those two are where the early bugs are.
Value types and reference types
A struct is a value: assigning it copies it. A class is a reference:
assigning it shares it. Every language has this distinction somewhere; Swift makes it the first
decision you take about a type, and pushes you hard toward struct.
The reason it matters in a UI app is that SwiftUI compares values to decide what to redraw. A model that can be mutated behind the framework's back is a model whose changes it cannot see.
struct CartItem: Identifiable, Equatable, Hashable {
let lineID: UUID
let productID: UUIDString
let productName: String
let productType: ProductType
let imageURL: String?
let size: SizeName
let basePrice: Double
let crustID: UUIDString?
let crustName: String?
let crustPriceDelta: Double
let toppings: [Topping]
var quantity: Int
var id: UUID { lineID }
// initialiser and configuration helpers omitted
One line of a shopping basket. Handing it to another type hands over a copy, so nothing can mutate a line the cart still believes it owns. That property is what lets the cart's rules — lesson 13 — be trivially correct, and it is the single biggest difference from the React version of this same app, where immutability is a discipline the author has to maintain by hand.
Note let on everything except quantity. In a struct, let
means the property cannot change after initialisation, and the compiler enforces it. Reach
for class when you genuinely need shared identity — the stores in lesson 4 are classes
for exactly that reason.
Optionals
A type either holds a value or it does not, and the type says which. String always
has a string; String? may be nil. There is no null hiding inside a
non-optional, which removes an entire category of crash.
The cost is that you have to say what happens when it is missing, every time. Swift gives you several ways, and choosing well is most of writing idiomatic Swift:
var displayName: String { "\(brand ?? "Card") •••• \(last4 ?? "????")" }
var expiryLabel: String {
guard let expMonth, let expYear else { return "Expiry unknown" }
return String(format: "Expires %02d/%d", expMonth, expYear)
}
Two techniques in six lines. ?? supplies a default inline, which is right when the
fallback is obvious and local. guard let unwraps and leaves early when it cannot —
and the unwrapped values stay in scope for the rest of the function, which if let does
not give you.
Prefer guard for preconditions. It keeps the happy path
unindented and puts the failure next to the condition that caused it, rather than at the bottom of
a pyramid.
Optional chaining and map
var singleLine: String {
let street = line2.map { "\(line1), \($0)" } ?? line1
return "\(street), \(city), \(state) \(postalCode)"
}
line2 is an optional second address line. map on an optional runs the
closure only if there is a value and stays nil otherwise — so this reads as "if there
is a second line, join it on; otherwise just the first". Written with if let it would
take four lines and a mutable variable.
The one to avoid
! force-unwraps: it asserts there is a value and crashes if there is not. It has
legitimate uses — a resource you ship in your own bundle, a regular expression literal you wrote —
and it is the single most common cause of crashes in iOS apps. The demo app's linter treats it as an
error in app code and allows it in tests, where a crash is a clear test failure.
Enums are not just constants
A Swift enum is a closed set of cases, and switch over one must be exhaustive.
That exhaustiveness is the feature: add a case and every place that handles the type stops
compiling until somebody deals with it.
enum HTTPMethod: String, Equatable {
case get = "GET"
case post = "POST"
case put = "PUT"
case patch = "PATCH"
case delete = "DELETE"
}
Backing it with String gives each case a raw value for the wire, and
HTTPMethod(rawValue:) to parse one back. The gain over a bare String is
that Endpoint(method: "POTS", …) cannot be written.
Enums can carry behaviour too, which is where they stop resembling constants:
enum SizeName: String, Codable, CaseIterable, Hashable {
case small = "SMALL"
case medium = "MEDIUM"
case large = "LARGE"
/// "MEDIUM" reads badly in a button; "Medium" does.
var displayName: String { rawValue.prefix(1) + rawValue.dropFirst().lowercased() }
}
CaseIterable synthesises allCases, which is how a picker builds its
options without a second list to keep in sync.
Associated values
A case can carry data, and different cases can carry different data. This is the feature that replaces a great deal of defensive checking:
enum ViewState<Value> {
case idle
case loading
case loaded(Value)
case empty
case failed(message: String, isRetryable: Bool)
// accessors and constructors omitted — lesson 11
Three booleans — loading, failed, has-data — describe eight combinations, five of which are nonsense. This describes five states, all of them real. Lesson 11 makes the argument properly; for now, notice that "loading and failed" is not something you can write down.
Protocols and extensions
A protocol is a set of requirements a type can declare it meets. Unlike a base class, a
struct can adopt one and adopt several, which is why Swift leans on protocols where
other languages reach for inheritance.
Look back at struct CartItem: Identifiable, Equatable, Hashable. Those three are
not inheritance — they are capabilities. Identifiable means it has an id,
which is what lets SwiftUI track it in a list. Equatable and Hashable are
synthesised: the compiler writes them for you when every stored property already
conforms.
Extensions add to an existing type, including one you do not own:
extension String {
/// Used everywhere a field is checked, because " " is not a name and `.isEmpty` says it is.
var trimmed: String { trimmingCharacters(in: .whitespacesAndNewlines) }
}
Now every string in the app has .trimmed. Used carelessly this becomes a junk
drawer, so the rule worth holding is: extend a type with something that genuinely belongs to it,
not with your feature's logic.
Closures, and the capture that leaks
Closures are functions you can pass around, and Swift's trailing-closure syntax is why SwiftUI reads the way it does. The part that bites is capture.
A closure holds a strong reference to what it captures. If an object owns a closure that captures that same object, neither is ever released:
dismissalTasks[toast.id] = Task { [weak self] in
try? await Task.sleep(for: self?.visibleDuration ?? .seconds(3))
guard !Task.isCancelled else { return }
self?.dismiss(toast.id)
}
[weak self] breaks the cycle, and self becomes optional inside. The
rule of thumb: a closure stored as a property, or one that outlives the call that created it, wants
[weak self]. One passed to map and used immediately does not.
Errors
Swift's error handling is explicit and checked. A function that can fail is marked
throws, every call to it is marked try, and the compiler will not let you
forget either.
enum APIError: LocalizedError, Equatable {
case api(status: Int, message: String, body: APIErrorBody?)
case unauthorized
case network(message: String)
case decoding(message: String)
// messages, field errors and retryability omitted — lesson 11
An error type is usually an enum, because the cases are the outcomes. Note what these cases are: not the places the code went wrong, but the decisions a caller can make. Lesson 11 builds this one out.
try? converts a throwing call into an optional, discarding the error —
try? await Task.sleep(…) above is doing exactly that, because a cancelled sleep is not
information anyone needs. try! crashes on failure, and deserves the same suspicion as
force-unwrapping.
Generics and Codable
Generics let a type work over another type without losing track of which one. The paginated envelope this app's API returns is the clearest small example:
struct Page<T: Decodable>: Decodable {
let content: [T]
let totalElements: Int
let totalPages: Int
let number: Int
let size: Int
}
Page<Order> and Page<Product> are one type, and
page.content is correctly typed in both. The constraint T: Decodable is
what makes the last line work: Decodable conformance is synthesised for
Page automatically, as long as whatever T turns out to be is decodable
too.
Codable is Encodable & Decodable, and it is how JSON becomes a
Swift value. Declare the conformance and the compiler writes the parsing, matching property names to
JSON keys. Where a name has to differ, the type declares CodingKeys — visible at the
model, rather than implied by a global setting somewhere.
The failure mode to know about: decoding throws if a required property is missing, and a property declared non-optional is required. A server that starts omitting a field breaks your app at the parse step, not at the use site. That is usually the behaviour you want — it fails at the boundary, loudly, rather than producing a half-built value — but it means the model is a contract and changing it is a real decision.
Access control, briefly
Folders are not modules in Swift — every file in a target shares one namespace. Visibility is
per type and per member: private (this declaration), fileprivate (this
file), internal (the default, the whole target), public and
open for frameworks.
The one worth knowing is private(set): readable anywhere, writable only from inside.
It is how the stores in the next lesson expose their state without letting a view corrupt it.
What to skip for now
Swift has a great deal more, and almost none of it is needed to build a screen. Property
wrappers, result builders, some and any, associated types, operator
overloading and macros are all worth learning eventually and none of them are worth learning today.
You use several of them constantly without writing one — @State is a property wrapper
and a SwiftUI body is a result builder, which is the point: the language features are
there so the framework can read the way it does.
Next
That is enough Swift to read the rest of this track. The next lesson starts SwiftUI proper — why a View is a value rather than an object, how layout is negotiated between a parent and its children, and why modifier order is not a matter of taste.