iOS Developer interview prep
A spaced-repetition deck of 153+ iOS Developer interview questions — organised by topic and difficulty, and resurfaced right before you'd forget. Preview a few cards below, then sign in to study the whole track on an Anki-style SM-2 schedule.
Free · sign in with GitHub · your progress stays yours.
What's covered
Every topic in this track, grouped the way you'd study it.
Swift & the Type System
12 cardsMemory & ARC
9 cardsConcurrency: GCD & Swift Concurrency
11 cardsUIKit & Layout
10 cardsSwiftUI
8 cardsiOS App Architecture
8 cardsNetworking & Persistence
9 cardsPerformance & Tooling
8 cardsCS Fundamentals
43 cardsBehavioral
35 cardsSample questions
A few cards from the deck — reveal each answer, then sign in to study the full set on a schedule.
What are the real differences between a struct and a class in Swift?
What are the real differences between a struct and a class in Swift?
Short answer: A struct is a value type: it is copied on assignment and when passed to a function. A class is a reference type: variables share one instance. Inheritance, deinit and identity (===) exist only for classes; mutating a struct requires var and mutating methods.
In depth:
struct |
class |
|
|---|---|---|
| Semantics | copy of the value | shared reference |
| Inheritance | ✗ (protocols only) | ✓ |
deinit / === |
✗ | ✓ |
| Mutation | mutating + var |
always, even via let |
| ARC | no retain/release (unless it holds references) | yes |
| Initializer | memberwise for free | you write it |
- Default to struct — predictable copying, no races on shared state, cheaper for ARC.
- Class — when you need shared identity (a cache, a service), inheritance from the NSObject world, or
deinitto release a resource.
⚠️ Common mistake: "structs always live on the stack." No: a struct stored in a class property, captured by an escaping closure, or boxed in an existential with a large payload lives on the heap. Value type defines copy semantics, not memory placement.
How does ARC work, and how is it different from a garbage collector?
How does ARC work, and how is it different from a garbage collector?
Short answer: ARC is automatic reference counting: the compiler inserts retain/release calls around strong-reference operations at compile time. An object dies exactly when its reference count hits zero — deterministically and with no pauses. The price: ARC cannot collect reference cycles.
In depth:
| ARC | Tracing GC | |
|---|---|---|
| Who decides | compiler: inserts retain/release | runtime: traverses the object graph |
| Moment of release | deterministic: count = 0 | non-deterministic, at collection time |
| Pauses | none | stop-the-world / incremental |
| Cycles | not collected — needs weak/unowned | collected automatically |
| Overhead | atomic count increments | heap headroom + CPU for tracing |
- Compiler, not runtime — a retain is inserted when a new strong reference appears, a release when it leaves scope; the optimizer removes redundant matched pairs.
- Deterministic deinit — the resource is freed immediately, which is why deinit is suitable for closing files, invalidating timers, unsubscribing.
- The price — retain cycles remain the programmer's responsibility.
⚠️ Common mistake: calling ARC "a garbage collector, just faster." ARC doesn't scan memory at runtime — all the work is placed by the compiler ahead of time, which is why there are no pauses and why cycles don't get found on their own.
How does a serial queue differ from a concurrent one, what are main and global — and why is a queue not a thread?
How does a serial queue differ from a concurrent one, what are main and global — and why is a queue not a thread?
Short answer: A queue is a "list of tasks" abstraction, not a thread: GCD multiplexes queues over a shared thread pool. A serial queue runs tasks strictly one at a time in FIFO order; a concurrent one runs several in parallel. Main is the only queue with the contract "I execute on the main thread"; global queues are system concurrent queues, one per QoS class.
In depth:
| Queue | Kind | What's special |
|---|---|---|
DispatchQueue.main |
serial | bound to the main thread; all UI |
DispatchQueue.global(qos:) |
concurrent | system-owned, shared app-wide |
DispatchQueue(label:) |
serial by default | your own; attributes: .concurrent makes it parallel |
| QoS | What it's for |
|---|---|
.userInteractive |
animations, gesture response — "right now" |
.userInitiated |
the user is waiting for a result (opening a screen) |
.utility |
long work with progress (file download) |
.background |
invisible work: sync, indexing |
- Queue ≠ thread — a serial queue doesn't own a thread: its tasks may run on different pool threads, just never simultaneously.
- QoS drives scheduling — QoS tells the system how much CPU and energy to grant:
.userInteractivejumps the line,.backgroundcan wait.
⚠️ Common mistake: "creating a queue creates a thread." No: thousands of queues are served by a dozen pool threads. But blocking many threads with sync waits is the road to thread explosion.
The UIViewController lifecycle: in what order are the methods called, and what belongs where?
The UIViewController lifecycle: in what order are the methods called, and what belongs where?
Short answer: loadView creates the view → viewDidLoad — one-time setup (no geometry yet!) → viewWillAppear — refresh before every presentation → viewWillLayoutSubviews/viewDidLayoutSubviews — the only places where frames are valid → viewDidAppear — animations and expensive starts → viewWillDisappear/viewDidDisappear.
In depth:
loadView ─► viewDidLoad ─► viewWillAppear ─► viewWillLayoutSubviews
(create (once) (every show) │ layout pass
view) ▼
viewDidDisappear ◄─ viewWillDisappear ◄─ viewDidAppear ◄─ viewDidLayoutSubviews
(on screen) (frames are valid!)
viewDidLoad— once per view controller's life: add subviews, constraints, subscriptions. The view has no real size yet.viewWillAppear— every presentation: refresh data, bar state.viewDidLayoutSubviews— bounds are current: corner rounding,CAShapeLayerpaths, size-dependent layout. Called many times — keep it cheap.viewDidAppear— the view is on screen: start animations, expensive work, analytics.viewWillDisappear/viewDidDisappear— stop timers, save state.
⚠️ Common mistake: reading view.bounds in viewDidLoad — you get the storyboard size or zeros, not the device's real geometry. Anything size-dependent belongs in viewDidLayoutSubviews.
Why are SwiftUI Views structs, and why isn't constantly recreating them expensive?
Why are SwiftUI Views structs, and why isn't constantly recreating them expensive?
Short answer: A View is not a view in the UIKit sense but a lightweight value-description: a blueprint of what to show. Structs are cheap to create and throw away, so SwiftUI freely recreates them on every body recomputation. The long-lived part — the render tree (attribute graph) — lives behind the scenes; that's where state and identity are kept.
In depth:
- Blueprint, not instance — a UIView is a heavyweight object with a lifecycle and a layer; a View struct is a couple of fields plus body, holding no resources.
- State lives outside — @State is stored by the framework and attached to the view's identity in the tree, not to the struct: the struct dies, the value survives.
- Diffing — SwiftUI compares the new description with the old one and minimally mutates the persistent tree; only the difference reaches the pixels.
body → View values (cheap, recreated every time)
│ diff
▼
attribute graph / render tree (long-lived:
@State, identity, animations)
⚠️ Common mistake: equating "recreating structs" with "redrawing the UI." Producing descriptions costs pennies; rendering is what can be expensive, and it happens only where the diff found a change.
Why does Apple-style MVC turn into Massive View Controller, and how do you slim a VC without changing the architecture?
Why does Apple-style MVC turn into Massive View Controller, and how do you slim a VC without changing the architecture?
Short answer: Because in UIKit the view controller is the default assembly point for everything: view lifecycle, layout, the table's data source, navigation and network calls. You slim it by extracting responsibilities into separate objects — no architecture change required.
In depth:
- Diagnosis — Apple's MVC isn't bad per se; the problem is that the "C" happily accepts any code, and without discipline the view controller grows to thousands of lines.
- What to extract:
| Responsibility | Where it goes |
|---|---|
UITableViewDataSource / delegate |
a dedicated data source object |
| A screen section with its own logic | a child view controller |
| Networking, cache, analytics | plain service objects |
| Subview configuration | the views themselves (configure(with:)) |
- Result — the view controller stays a conductor: it wires services, views and navigation together but doesn't do all the work itself.
⚠️ Common mistake: answering "MVC is a bad architecture, that's why everyone moved to MVVM." What's bad is an undisciplined implementation: without extracting responsibilities the same overload happens to a ViewModel too.
Ready to make it stick?
Start your first session in under a minute. Your future self, mid-interview, will thank you.
Questions about this track
How should I prepare for a iOS Developer interview?
Study the concepts you'll be asked to explain, not just the ones you can code. RecallDeck's iOS Developer track gives you 153+ curated interview questions and resurfaces each one with an Anki-style SM-2 schedule right before you'd forget it — so the answers are still there under pressure on interview day.
What topics does the iOS Developer track cover?
The iOS Developer track is organised into the core areas iOS Developer interviews actually test, grouped by topic and by difficulty (Concept, Junior, Middle, Senior). You can preview the full outline and sample questions above before signing in.
Is spaced repetition effective for iOS Developer interview prep?
Yes. Actively recalling an answer and grading yourself honestly builds far more durable memory than re-reading notes. RecallDeck schedules each iOS Developer card to reappear at the moment you're about to forget it, so your daily reviews shrink while your recall holds.
Is the iOS Developer track free?
Yes — the iOS Developer track and the full SM-2 scheduler are free, with 20 new cards a day. Sign in with GitHub and your progress syncs to your account. RecallDeck Pro ($5/month or $29/year) raises the daily budget and adds cram mode.