iOS – The Swift You Need First

July 20, 20268 min readUpdated 9/18/2026

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.