Engineering Interviews
Senior iOS interview questions: the 5 follow-ups that decide the loop
Your first answer rarely decides a senior iOS interview. The follow-up does. Five follow-ups, the answer that loses the point, the one that earns it, and what to say when you wobble.
6 min read
The candidate was good. He explained actors cleanly, used the right words, stayed calm. Then I asked one more question about the same code. Forty seconds later the answer had fallen apart, and so had the interview.
That moment is what this article is about. After 15 years in mobile and 500+ technical interviews from the hiring side, the pattern I trust most is this: in a senior iOS interview, the correct first answer is the entry fee. The score is decided by the follow-up.
A first answer tells me what you have read. The follow-up tells me what you have shipped.
- Senior iOS loops are graded on the second answer, not the first.
- Every losing answer below is a correct rule, repeated past the point where it holds.
- The skill being tested is simple to name: knowing when your own answer stops being true.
The 5 follow-ups at a glance
| Topic | First question | The follow-up that decides it |
|---|---|---|
| Concurrency | How do you protect shared state? | Is it still safe across the await? |
| SwiftUI | Where does the view model live? | Why does the form reset? |
| Memory | How do you avoid retain cycles? | Does every closure need weak self? |
| Architecture | Why MVVM? | Where would you not use it? |
| System design | Design offline support. | Two devices edit offline. Who wins? |
1. Concurrency: is it still safe across the await?
The first question: "How would you protect this image cache from data races?"
Most senior candidates get this right: make it an actor. Then I show them their own code.
actor ImageCache {
private var images: [URL: UIImage] = [:]
func image(for url: URL) async throws -> UIImage {
if let cached = images[url] { return cached }
let image = try await Self.download(url) // suspension point
images[url] = image
return image
}
}The follow-up: "Ten cells ask for the same URL at once. How many downloads happen?"
The answer that loses the point: "One. It's an actor, so only one call runs at a time."
The answer that earns it: "Up to ten. Actors are reentrant. At the await, the actor is free to run the other calls, and each one finds the cache empty and starts its own download. There are no data races, but the check and the write aren't atomic. I'd store the in-flight task so later callers await the same work."
private var inFlight: [URL: Task<UIImage, Error>] = [:]
func image(for url: URL) async throws -> UIImage {
if let cached = images[url] { return cached }
if let running = inFlight[url] { return try await running.value }
let task = Task { try await Self.download(url) }
inFlight[url] = task
defer { inFlight[url] = nil }
let image = try await task.value
images[url] = image
return image
}What gets scored: the mid-level answer knows what actors prevent. The senior answer knows what they don't.
Recovery line: "Let me correct that. The actor stops data races, but anything I read before an await can be stale after it. I'd deduplicate the in-flight work."
2. SwiftUI state: why does the form reset?
The first question: "Where does this screen's view model live?"
The follow-up: "Users say the form clears whenever the parent refreshes. Why?"
struct ProfileForm: View {
@ObservedObject var model = ProfileModel() // a new instance on every parent re-render
var body: some View { /* ... */ }
}The answer that loses the point: "SwiftUI redraws the view, so the state is lost. I'd cache the values."
The answer that earns it: "The view creates an object it doesn't own. @ObservedObject doesn't keep the instance, so every time the parent re-evaluates, the child builds a new ProfileModel. Ownership has to be explicit: @StateObject for an ObservableObject, or @State for an @Observable model. SwiftUI then keeps one instance for the view's lifetime. If the parent owns it, the parent creates it and passes it down."
@StateObject private var model = ProfileModel() // created once, kept across re-rendersWhat gets scored: not the property wrapper. It's that you think in terms of who owns this object and how long it lives. That question is behind most SwiftUI bugs in production.
Recovery line: "My first answer treated the symptom. The real question is ownership: which view creates this object, and does SwiftUI keep it?"
3. Memory: does every closure need weak self?
The first question: "How do you avoid retain cycles in closures?" Almost everyone says [weak self].
The follow-up: "Does every closure need it? And is this one safe?"
task = Task { [weak self] in
guard let self else { return }
for await update in stream {
self.apply(update) // self is held strongly until the stream ends
}
}The answer that loses the point: "Yes, I always use weak self to be safe."
The answer that earns it: "No. A non-escaping closure, like the one passed to map, can't outlive the call, so it can't create a cycle. The risk is an escaping closure that stays alive through something self owns. This one looks safe but isn't: guard let self at the top turns the weak reference back into a strong one for the whole loop. If the stream never ends, self never deallocates. I'd unwrap inside the loop, and cancel the task when the owner goes away."
task = Task { [weak self] in
for await update in stream {
guard let self else { return } // strong only for this iteration
self.apply(update)
}
}What gets scored: "always" is the word that costs the point. Seniors apply the rule where the lifetime demands it, and can say why.
Recovery line: "I overstated that. Weak self matters when the closure can outlive the call. Here the guard undoes it, so let me move it inside the loop."
4. Architecture: where would you not use it?
The first question: "Which architecture do you use, and why?" The candidate explains MVVM well: testable view models, views that only render, clear separation.
The follow-up: "Give me a screen where you wouldn't use it."
The answer that loses the point: "I use MVVM everywhere, for consistency."
The answer that earns it: "A settings screen with three toggles bound to user defaults. A view model there adds a layer and tests that check nothing real. I keep the pattern for screens with async loading, business rules or state worth testing, and keep trivial screens trivial. For the team, I'd write that rule down, so consistency means the same rule, not the same boilerplate."
What gets scored: not the pattern you pick. Whether you can name its cost. An engineer who can't argue against their own architecture is repeating it, not choosing it.
Recovery line: "Let me make that less absolute. The pattern pays off where there's logic to test. Where there isn't, it's cost without a return."
5. System design: two devices edit offline. Who wins?
The first question: "Design offline support for a notes app." Strong candidates cover a local store, a sync queue, retries with backoff and a visible sync state.
The follow-up: "The same note is edited on a phone and a tablet, both offline. They reconnect. What does the user see?"
The answer that loses the point: "Last write wins, using the timestamp."
The answer that earns it: "Last write wins silently deletes one person's edit, and device clocks can't be trusted to order two offline edits. So it depends on what the data is worth. For a title, last write wins may be fine. For the body, I'd keep a version per note, detect that both edits started from the same version, merge the changes that don't overlap, and when they do, keep both and let the user choose. Losing someone's text without telling them is the one outcome I wouldn't ship."
What gets scored: senior system design isn't a bigger diagram. It's knowing where the data can be lost, and deciding that on purpose.
Recovery line: "Last write wins was too quick. Let me walk through what it loses, and choose the rule per field based on what the data is worth."
The pattern behind all five
Read the losing answers together: "it's an actor", "SwiftUI redraws", "always weak self", "everywhere for consistency", "last write wins".
Every one of them is a correct rule, repeated past the point where it holds. The follow-up exists to find that point. None of the winning answers needed rare knowledge. They needed one habit: asking "when is this not true?" before the interviewer does.
A 10-minute drill before your next loop
- Pick one topic from your last interview.
- Say your first answer out loud, on a timer, in under 30 seconds.
- Ask yourself: "When does that stop being true?"
- Answer out loud. Write down the one case you reached for and didn't have.
- Change one fact (offline, two users, a long await, a parent refresh) and answer again.
Reading answers feels like progress. Saying them under a changed constraint is the practice that shows up in the room. The Android version of this list is the 5 follow-ups that decide a senior Android loop.
Questions engineers ask about senior iOS interviews
What do interviewers look for in a senior iOS interview?
Reasoning under pushback. A correct definition gets you to the next question. What separates senior from mid-level is naming the trade-off, the edge case and the cost of your own choice without being led to it.
How do I prepare for follow-up questions?
Practise out loud, not by reading. For every answer you prepare, write the follow-up that breaks it and the sentence you would say next. Then change one constraint and answer again.
How long does it take to prepare for a senior iOS loop?
With weeks, cover the full loop: Swift and concurrency, SwiftUI, memory, architecture, system design and the behavioral round. With days, focus on the highest-frequency questions and rehearse the follow-ups out loud.
