← Back to hub
Swift · iOS interview prep

Swift fundamentals
from first principles

A practical map of Swift’s core choices: what each feature is made of, which assumptions are hiding underneath it, how the pieces recombine in real apps, and how to reason when an interviewer changes the facts.

DecomposeAudit assumptionsRecombineExperimentSwift 6.3 · Xcode 26.6 baseline
Interview transfer · compressed output

The 60-second answer

Use this as the answer shell. The rest of the page explains why each sentence is true.

I model the product’s invariants first. A data model represents the wire or domain contract; a UI model represents what the screen needs, including loading, success and failure. I prefer an enum when states are mutually exclusive so impossible combinations are harder to represent.

For values, I choose a struct when independent copies are the semantic contract, and a class when identity, shared ownership, inheritance or framework interop is real. I do not infer stack allocation or performance from that choice. Swift collections keep value semantics while commonly using copy-on-write, so I measure before making a speed claim.

I use Optionals to model absence and choose guard, if let or ?? from the product’s failure policy. I treat try?, try! and ! as information and safety decisions, not just shorter syntax.

Protocols define contracts and make dependencies replaceable; generics preserve type relationships when one implementation can serve many concrete types. At a serialization boundary I use Codable for the ordinary mapping, explicit keys and custom decoding when the wire contract differs, then map DTOs into domain/UI models.

For closures and SwiftUI, I draw ownership and source-of-truth edges. A strong capture is only a leak when it closes a retained cycle. @State/@Binding describe value ownership and access, while object wrappers describe reference-model ownership or observation. Finally, I debug by tracing the failing state, error path, executor and lifetime instead of hiding the symptom with a force unwrap.

D · Decompose

What Swift fundamentals are made of

Most “Swift questions” are really questions about one invariant. Name the invariant before naming the feature.

The deeper question: What must remain true when data is copied, absent, decoded, mutated, observed, failed, or kept alive? Swift’s syntax is the surface; the interview signal is whether you can trace the underlying contract.
Model

Meaning and state

What does this value represent? Which states are valid, and which should be impossible?

Ownership

Identity and lifetime

Does a copy become independent, or do two names refer to one identity?

Absence

Optionality

Is a value missing, invalid, delayed, or intentionally unknown?

Behavior

Contracts

Which operations are promised, and can a concrete implementation be swapped?

Function value

Closures

What does the function capture, who stores it, and when can it stop being called?

Boundary

Wire data

Which keys, nulls, dates and type changes are compatible with the app?

Invariant

Collections

Do we need order, uniqueness, keyed lookup, or all three through separate views?

Exclusivity

Enums

Which states cannot coexist, and what data belongs to each case?

Failure

Error policy

Should the caller recover, propagate, convert, retry, or crash during development?

Reuse

Generics

Which type relationship must be preserved across the reusable operation?

Source of truth

SwiftUI state

Who owns the state, who may mutate it, and who merely observes it?

Evidence

Debugging

What changed, what invariant broke, and which tool can distinguish the likely causes?

1 · Type modeling · R

Model states, not boolean soup

Separate the shape of data from the screen’s state. A wire DTO may mirror an API; a domain model enforces product meaning; a UI model makes rendering decisions explicit.

struct UserDTO: Decodable {
    let id: Int
    let displayName: String?
}

struct User {
    let id: Int
    let name: String
}

enum LoadState<Value> {
    case idle, loading
    case loaded(Value)
    case failed(LoadError)
}
Audit assumption: “A screen is loading” and “a screen has data” are not independent booleans. If both can be true in the same model, decide whether that is a real state or an accidental combination.

Loading state, live

Tap a state. The model makes the legal payload explicit instead of scattering flags across the view.

2 · Value and reference semantics · D → A → R

Copying is a semantic question before it is a performance question

Structs and enums have value semantics: assigning a value gives the program an independent value to mutate observably. Classes have identity: two variables can point at one instance. The compiler and standard library may optimize the physical representation.

QuestionStruct / enumClass
What does copying mean?Independent value behaviorAnother reference to the same identity
Choose it when…Meaning is data; copies should not share mutationIdentity, shared ownership, inheritance, UIKit/ObjC interop
What is not guaranteed?“Always stack allocated” or “always faster”“Every strong reference is a leak”
struct Profile { var name: String }
var a = Profile(name: "Ada")
var b = a
b.name = "Grace"       // a.name is still "Ada"

final class ProfileBox { var name: String; init(_ name: String) { self.name = name } }
let x = ProfileBox("Ada")
let y = x; y.name = "Grace"   // x.name is now "Grace"
Nested-reference edge: a struct can contain a class reference. The outer value is copied, but both copies may deliberately point at the same inner identity. That is aliasing, not a failure of the outer type’s semantics.

Mutation experiment

Copy-on-write: Array, Dictionary, Set and String can share storage until a mutation needs an independent buffer. That is an implementation strategy preserving value semantics, not a promise about every custom type.

3 · Optionals · A

Absence is data; unwrapping is policy

An Optional says “there may be a value.” The right syntax depends on what absence means here: branch, early exit, fallback, propagate, or programmer error.

ToolUse whenWhat happens when absent / failing
if letThe rest of this branch needs the valueSkip the branch
guard letThe function cannot continue meaningfullyExit through else; value stays available afterward
??A domain-safe default existsUse the fallback
try?All failure detail can intentionally collapse to absenceConvert any thrown error to nil
try! / !Only when the invariant is truly guaranteed and a crash is the right responseTrap if the assumption is false
func subtitle(for user: User?) -> String {
    guard let user else { return "Sign in to continue" }
    return user.name
}

let name = payload["name"] ?? "Unknown" // safe product fallback?
let value = try? decoder.decode(UserDTO.self, from: data) // loses why
Optional policy experiment
Red-team question: If a missing value means “not authorized,” should ?? "Unknown" silently show a fake identity? The syntax is not the policy; the product meaning decides.
4 · Protocol-oriented design · R

Protocols are contracts; dependency injection is ownership of the choice

A protocol describes capability. Dependency injection decides who supplies the capability. A mock is valuable because the boundary is replaceable and the test owns the behavior, not because every type needs a protocol.

protocol UserServing {
    func fetchUser() async throws -> User
}

struct LiveUserService: UserServing { ... }
struct StubUserService: UserServing {
    let result: Result<User, Error>
    func fetchUser() async throws -> User { try result.get() }
}

struct ProfileModel {
    let service: any UserServing
}
Audit assumption: “Protocol-oriented” does not mean “protocol everywhere.” Add an abstraction when substitution, consumer-owned policy or independent change pays for the indirection. Concrete injection is still injection.

Dependency choice, live

Protocol extensions can provide shared behavior, but a default implementation should not accidentally hide a required policy or make a mock harder to control.

5 · Closures and memory · D → A → E

Escaping is about lifetime; a cycle is about a retained loop

Ask one ownership question: who stores the closure? A one-shot service can retain a closure temporarily without forming a cycle. A repeating timer or an object-stored callback can close the graph.

// The object stores the closure; the closure captures the object.
final class Screen {
    var onTap: (() -> Void)?
    func connect() { onTap = { [weak self] in self?.submit() } }
}

load { [weak self] result in
    guard let self else { return }
    self.render(result)
}

// Use unowned only when the lifetime guarantee is real.
unowned let parent: Parent
Important distinction: [weak self] prevents one ownership cycle, but it can also skip required work after the screen disappears. Choose weak/unowned from a lifetime contract and completion policy, not from a “weak is safer” slogan.

Retain graph, live

Switch the capture policy, then release the external owner. The experiment shows why a strong capture can leak only when the object stores the closure.

Screenstrong count 2
stored closurestrong capture
aliveweak edgedeallocated
6 · Serialization · A → R → E

Codable is a compatibility contract, not a magic API

Keep wire shape and app meaning separate when they change at different speeds. Synthesis handles ordinary mappings; CodingKeys, decoder strategies and custom decoding express explicit compatibility policy.

struct UserDTO: Decodable {
    let id: Int
    let displayName: String?

    enum CodingKeys: String, CodingKey {
        case id
        case displayName = "display_name"
    }
}

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let users = try decoder.decode([UserDTO].self, from: data)
Payload changeSynthesized decodingPolicy question
Unknown keyUsually ignoredSafe forward compatibility?
Missing optional key / null optionalnilDoes absence have product meaning?
Missing required keythrowsReject, migrate, or supply a declared default?
Wrong type / null for non-optionalthrowsDo not hide a contract break as “empty”
Payload experiment

After decoding, map DTOs to domain models when the API’s optionality, naming, date representation or failure semantics should not leak into the rest of the app.

7 · Collections · D → A

Choose the collection from the invariant

Start with the operation the product needs, then choose the representation. Complexity is a useful prediction under stated assumptions, not a universal speed guarantee.

CollectionInvariantUseful operationsWatch out for
ArrayOrdered sequence; duplicates allowedIteration, indexed access, stable UI orderMiddle insertion/removal shifts elements
SetUnique elementsMembership and union/intersection; expected average hash lookupElements need Hashable; do not use it to promise display order
DictionaryKey → value associationExpected average lookup by keyKeys need Hashable; duplicate insertion needs an explicit policy
var ids = Set<Int>()
let inserted = ids.insert(42).inserted // invariant: one 42

var byID: [Int: User] = [:]
byID[user.id] = user                  // keyed access

let visible = users.sorted { $0.name < $1.name } // order belongs to the view policy
Invariant experiment
What breaks if we use the wrong structure? A Set can remove duplicates but cannot preserve a promised first-seen display order by itself; an Array can preserve order but makes “does this exist?” a linear scan; a Dictionary gives keyed lookup but is not a UI ordering model.
8 · Enums and associated values · R

Make mutually exclusive states impossible to confuse

Associated values attach the data that belongs to a case. Pattern matching with switch forces the code to account for the state space.

enum LoadState<Value> {
    case idle
    case loading
    case loaded(Value)
    case failed(Error)
}

switch state {
case .idle: showPlaceholder()
case .loading: showSpinner()
case .loaded(let users): show(users)
case .failed(let error): show(error)
}

If a state needs several unrelated booleans to explain its payload, the enum’s cases may be too broad. If two cases carry the same meaning, the model may be duplicating policy.

9 · Error handling · A → R → E

Failure is part of the API contract

Use throws when the caller should decide how to recover or propagate. Use Result when the success/failure value must be stored, transformed or passed as data. async throws combines possible suspension with failure propagation; it does not mean “runs in the background.”

enum ProfileError: Error { case offline, invalidPayload }

func loadProfile() async throws -> Profile { ... }

do {
    let profile = try await loadProfile()
    render(profile)
} catch ProfileError.offline { showOffline() }
  catch { showRetry(error) }

let result: Result<Profile, ProfileError> = ...
Error propagation experiment
Audit assumption: “Handle the error” is not the same as “catch and print.” A strong answer names the recovery owner, the user-visible state, retry policy, cancellation behavior and what information must remain observable.
10 · Generics · D → R

Reuse the operation while preserving type relationships

A generic parameter says several values share a concrete type relationship. A constraint says what operations are legal. That is different from accepting any value behind a protocol existential.

func decode<T: Decodable>(_ type: T.Type, from data: Data) throws -> T {
    try JSONDecoder().decode(type, from: data)
}

func first<T>(_ values: [T]) -> T? {
    values.first
}

// The caller keeps the concrete type relationship.
let user: UserDTO = try decode(UserDTO.self, from: data)
ChoiceQuestion it answersInterview signal
Generic <T>Which concrete types must stay related?Reusable code with static checking
any PDo I need to store arbitrary conformers behind one interface?Runtime substitution at an abstraction boundary
Concrete typeIs there no real variation to model?Less indirection when abstraction does not pay rent
Constraint experiment
11 · SwiftUI state management · A → R

Ask who owns the state before choosing a wrapper

SwiftUI may reevaluate body; the wrapper determines where state lives and who can mutate or observe it. The choice is ownership and data flow, not a list of “modern” keywords.

WrapperUse it when…Core question
@StateThe view owns local value stateDoes this value belong to this view’s identity?
@BindingA child edits state owned by a parentCan the child mutate through a controlled projection?
@StateObjectThis view creates/owns an ObservableObjectWho owns the reference model across redraws?
@ObservedObjectThe reference model is supplied by another ownerAm I observing rather than creating it?
@ObservableUsing the Observation framework’s access-tracked modelWhich properties does this view actually read?
@EnvironmentObjectA legacy/reference model is injected through the environmentWhere is the required dependency inserted?
// Value owned by the view; child receives a writable projection.
struct Parent: View {
    @State private var isOn = false
    var body: some View { ToggleRow(isOn: $isOn) }
}

// Observation model: ownership still matters; the macro does not choose it.
import Observation
@MainActor @Observable
final class ScreenModel { var state: LoadState = .idle }
Ownership experiment

Version note: @Observable belongs to the Observation framework and is available from iOS 17. This page is reviewed against the repo baseline of shipping Swift 6.3, Xcode 26.6 and iOS 26.5 SDK; availability and deployment targets still need checking in a real app.

12 · Code reading and debugging · E

Trace the failing invariant, not just the crashing line

When reading unfamiliar Swift, ask four questions in order: what state is legal, what can be absent or fail, who owns the value/closure/task, and which executor/lifecycle event is allowed to mutate or release it?

// Risky: three different failures are hidden.
func refresh() {
    let title = try? service.fetchTitle()  // error becomes nil
    label.text = title!                 // crash if nil
    Task { self.save() }                // inspect lifetime + executor
}

// Safer shape: explicit state, policy and ownership.
@MainActor
func refresh() async {
    state = .loading
    do { state = .loaded(try await service.fetchTitle()) }
    catch is CancellationError { }
    catch { state = .failed(.network) }
}
Tool boundary: a compiler isolation diagnostic, Main Thread Checker, Thread Sanitizer, Memory Graph Debugger and Instruments answer different questions. A clean result from one does not prove every other invariant.
Audit assumptions · interview phrasing

Don’t say this, say that

These are the shortcuts a red-team interviewer will usually probe.

“Structs are always on the stack and faster.”
“Structs give value semantics; representation and speed depend on the type and context, so I measure.”
“Any strong closure capture is a retain cycle.”
“A cycle needs a retained loop; I ask whether the object stores the closure and draw the edges.”
“Use weak everywhere.”
“Use weak when the target may disappear first; use unowned only when the lifetime guarantee is real.”
“try? handles the error.”
“try? intentionally collapses error detail to nil; I use it only when that loss matches the policy.”
“Codable makes API changes safe.”
“Codable implements a declared contract; missing, null and incompatible values still need compatibility policy.”
“Dictionary lookup is always O(1).”
“Hash lookup is expected average O(1) under assumptions; keys, collisions, resizing and the metric still matter.”
“@Observable owns the model for SwiftUI.”
“Observation changes how reads are tracked; the view hierarchy still needs an explicit owner and injection path.”
“async means background.”
“await is a suspension point; executor isolation and the work’s cost decide responsiveness and execution context.”
Experiment · retrieval and red-team

Drill

Six follow-ups. Predict the result before you look for the rule.

Rapid recall · morning-of checklist

Can you derive the answer?

Tick these off only when you can explain the why, not just repeat the phrase. Progress is saved in this browser.

Likely next topics

Copy-on-write internals · some vs any · actors and Sendable · property wrappers and macros · custom decoding migrations · Swift Testing · Instruments and MetricKit · SwiftUI navigation and task lifetime

Sources · consulted 2026-09-03

Sources and further reading

Primary documentation used to verify the semantics and API roles in this page. Version-sensitive claims are marked in the page and should be rechecked during the repo’s annual platform refresh.

Swift.org · accessed 2026-09-03

Value/reference semantics, identity and the distinction between semantic behavior and representation.

Swift.org · accessed 2026-09-03

Strong, weak and unowned references, object lifetime and retain-cycle reasoning.

Swift.org · accessed 2026-09-03

Function values, escaping closures and capture behavior.

Swift.org · accessed 2026-09-03

Optionals, forced unwrapping and basic type behavior.

Swift.org · accessed 2026-09-03

Contracts, conformance, protocol extensions and abstraction boundaries.

Swift.org · accessed 2026-09-03

Associated values, mutually exclusive cases and pattern matching.

Swift.org · accessed 2026-09-03

throws, do/catch and propagation of recoverable failures.

Swift.org · accessed 2026-09-03

Generic parameters, constraints and reusable type-safe code.

Swift.org · accessed 2026-09-03

Array, Set and Dictionary invariants and collection selection.

Apple Developer Documentation · accessed 2026-09-03

Codable, custom keys and decoding boundaries.

Apple Developer Documentation · accessed 2026-09-03

SwiftUI-owned state and local value-state persistence.

Apple Developer Documentation · accessed 2026-09-03

Two-way access to state owned elsewhere.

Apple Developer Documentation · accessed 2026-09-03

Ownership versus observation for ObservableObject reference models.

Apple Developer Documentation · accessed 2026-09-03

Reference-model injection through the SwiftUI environment.

Apple Developer Documentation · accessed 2026-09-03

Observation framework modeling and access-tracked observable properties.

ios-interview-kit · reviewed 2026-07-14

Local version context: Swift 6.3 and Xcode 26.6 shipping; newer beta versions remain separate from the baseline.