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.
What Swift fundamentals are made of
Most “Swift questions” are really questions about one invariant. Name the invariant before naming the feature.
Meaning and state
What does this value represent? Which states are valid, and which should be impossible?
Identity and lifetime
Does a copy become independent, or do two names refer to one identity?
Optionality
Is a value missing, invalid, delayed, or intentionally unknown?
Contracts
Which operations are promised, and can a concrete implementation be swapped?
Closures
What does the function capture, who stores it, and when can it stop being called?
Wire data
Which keys, nulls, dates and type changes are compatible with the app?
Collections
Do we need order, uniqueness, keyed lookup, or all three through separate views?
Enums
Which states cannot coexist, and what data belongs to each case?
Error policy
Should the caller recover, propagate, convert, retry, or crash during development?
Generics
Which type relationship must be preserved across the reusable operation?
SwiftUI state
Who owns the state, who may mutate it, and who merely observes it?
Debugging
What changed, what invariant broke, and which tool can distinguish the likely causes?
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) }
Loading state, live
Tap a state. The model makes the legal payload explicit instead of scattering flags across the view.
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.
| Question | Struct / enum | Class |
|---|---|---|
| What does copying mean? | Independent value behavior | Another reference to the same identity |
| Choose it when… | Meaning is data; copies should not share mutation | Identity, 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"
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.
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.
| Tool | Use when | What happens when absent / failing |
|---|---|---|
if let | The rest of this branch needs the value | Skip the branch |
guard let | The function cannot continue meaningfully | Exit through else; value stays available afterward |
?? | A domain-safe default exists | Use the fallback |
try? | All failure detail can intentionally collapse to absence | Convert any thrown error to nil |
try! / ! | Only when the invariant is truly guaranteed and a crash is the right response | Trap 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
?? "Unknown" silently show a fake identity? The syntax is not the policy; the product meaning decides.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 }
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.
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
[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.
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 change | Synthesized decoding | Policy question |
|---|---|---|
| Unknown key | Usually ignored | Safe forward compatibility? |
| Missing optional key / null optional | nil | Does absence have product meaning? |
| Missing required key | throws | Reject, migrate, or supply a declared default? |
| Wrong type / null for non-optional | throws | Do not hide a contract break as “empty” |
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.
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.
| Collection | Invariant | Useful operations | Watch out for |
|---|---|---|---|
Array | Ordered sequence; duplicates allowed | Iteration, indexed access, stable UI order | Middle insertion/removal shifts elements |
Set | Unique elements | Membership and union/intersection; expected average hash lookup | Elements need Hashable; do not use it to promise display order |
Dictionary | Key → value association | Expected average lookup by key | Keys 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
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.
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> = ...
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)
| Choice | Question it answers | Interview signal |
|---|---|---|
Generic <T> | Which concrete types must stay related? | Reusable code with static checking |
any P | Do I need to store arbitrary conformers behind one interface? | Runtime substitution at an abstraction boundary |
| Concrete type | Is there no real variation to model? | Less indirection when abstraction does not pay rent |
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.
| Wrapper | Use it when… | Core question |
|---|---|---|
@State | The view owns local value state | Does this value belong to this view’s identity? |
@Binding | A child edits state owned by a parent | Can the child mutate through a controlled projection? |
@StateObject | This view creates/owns an ObservableObject | Who owns the reference model across redraws? |
@ObservedObject | The reference model is supplied by another owner | Am I observing rather than creating it? |
@Observable | Using the Observation framework’s access-tracked model | Which properties does this view actually read? |
@EnvironmentObject | A legacy/reference model is injected through the environment | Where 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 }
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.
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) } }
Don’t say this, say that
These are the shortcuts a red-team interviewer will usually probe.
Drill
Six follow-ups. Predict the result before you look for the rule.
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 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.
Value/reference semantics, identity and the distinction between semantic behavior and representation.
Strong, weak and unowned references, object lifetime and retain-cycle reasoning.
Function values, escaping closures and capture behavior.
Optionals, forced unwrapping and basic type behavior.
Contracts, conformance, protocol extensions and abstraction boundaries.
Associated values, mutually exclusive cases and pattern matching.
throws, do/catch and propagation of recoverable failures.
Generic parameters, constraints and reusable type-safe code.
Array, Set and Dictionary invariants and collection selection.
Codable, custom keys and decoding boundaries.
SwiftUI-owned state and local value-state persistence.
Two-way access to state owned elsewhere.
Ownership versus observation for ObservableObject reference models.
Reference-model injection through the SwiftUI environment.
Observation framework modeling and access-tracked observable properties.
Local version context: Swift 6.3 and Xcode 26.6 shipping; newer beta versions remain separate from the baseline.