Engineering Interviews
iOS coding interview: what the live round actually tests
The iOS coding interview is rarely a puzzle. It is a small app task under a clock, and the score comes from how you build it, not whether it compiles on the first try. The five common tasks, what gets scored in each, and what to say when it breaks.
6 min read
The candidate had a strong CV and a clean GitHub. I asked her to load a list of users from an endpoint and show them in a list. Twenty minutes later the screen worked. She had not said a word the whole time, the error case printed to the console, and when I asked why the view model was not on the main actor, she did not know it mattered.
She did not pass. The code was fine. The round was not about the code.
After 15 years in mobile and 500+ technical interviews from the hiring side, this is what I can tell you about the iOS coding interview: it is a small app task under a clock, and I am scoring how you build, not what you end up with.
- Most iOS coding tasks are app work: a list, a model, a cache, a search field, a bug.
- Loading, error and cancellation states count as much as the happy path.
- Talking while you type is not a courtesy. It is half the signal.
The five tasks that come up most
Companies dress them up differently. Underneath, the same five shapes keep returning.
| Task | What it looks like | What I actually score |
|---|---|---|
| List screen with async loading | Fetch items, show a list, handle loading and errors | State modelling, main actor, error path |
| Parse JSON into models | Decode a payload with odd keys, dates or optional fields | Codable precision, failure handling |
| Image cache | Load thumbnails in a scrolling list without refetching | Concurrency safety, duplicate requests, memory |
| Debounced search | Search as the user types, without a request per keystroke | Cancellation, stale results |
| Fix a buggy snippet | Read 30 lines, find what is wrong, explain it | Reading code, naming the cause, not guessing |
Pure algorithm questions still appear at some companies. When they do, they are usually light: a dictionary, a set, a sort. If you prepare the five tasks above, you have prepared most of what an iOS coding test will ask.
1. The list screen: state before views
This is the most common iOS interview coding challenge, and the one candidates underrate. The list itself takes two minutes. The score lives in the states around it.
I watch for one decision early: do you model the screen state, or scatter booleans? Here is a sketch of what earns the point.
@MainActor
@Observable
final class UsersViewModel {
enum State {
case idle, loading
case loaded([User])
case failed(String)
}
private(set) var state: State = .idle
private let client: UsersClient
init(client: UsersClient) { self.client = client }
func load() async {
state = .loading
do {
state = .loaded(try await client.fetchUsers())
} catch is CancellationError {
// The view went away. Nothing to show.
} catch {
state = .failed("Could not load users.")
}
}
}Then the view switches on state and calls load() from .task, which cancels the work when the view disappears. That is the whole screen.
What gets scored: one enum instead of isLoading, error and users that can contradict each other. The view model isolated to the main actor, because it drives the UI. A client behind a protocol, so it can be faked. Cancellation treated as a normal outcome, not an error message.
What to say at the start: "I'll model the screen as one state enum first, then build the view on top. Happy path first, then errors, then empty." One sentence, and I already know you have shipped a screen like this.
2. JSON into models: say what can fail
The payload always has something awkward. A snake_case key. A date as a string. A field that is sometimes missing. The task is small on purpose. I want to see whether you read the data before you write the type.
What gets scored: matching optionality to the real data, not making everything optional to stop the crash. Using keyDecodingStrategy or CodingKeys on purpose. Setting a dateDecodingStrategy instead of storing a String. And one sentence about what happens when decoding fails: does one bad item kill the whole list, or do you skip it?
That last question separates levels. A mid-level answer decodes [User] and moves on. A senior answer says: "If one record is malformed, the whole array fails. For a feed I'd rather drop that item and log it. I'll mention it and keep the simple version for now." You do not have to build it. You have to see it.
3. The image cache: the race you have to see
The prompt sounds simple: thumbnails in a list, do not download the same image twice. Most candidates reach for a dictionary. Better candidates reach for an actor. The best ones notice that an actor alone does not stop two downloads of the same URL.
Actors are reentrant. While one call is suspended at the await, a second call for the same URL runs, misses the cache and starts its own download. The fix is to store the work in flight, not only the result.
actor ImageCache {
private var images: [URL: UIImage] = [:]
private var inFlight: [URL: Task<UIImage, Error>] = [:]
func image(for url: URL) async throws -> UIImage {
if let image = images[url] { return image }
if let task = inFlight[url] { return try await task.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: naming the reentrancy problem before I do. Knowing that a real cache needs a limit, so you mention NSCache or a count cap even if you do not build it. Knowing that a reused cell must not show the previous row's image when a slow download lands late.
What to say: "The actor protects the dictionary, but not the gap across the await. I'll track in-flight tasks so ten cells asking for one URL share one download."
4. Debounced search: cancel, do not just delay
Search as the user types. The obvious bug is a request per keystroke. The less obvious one is worse: results arriving out of order, so the list shows matches for "sw" after the user typed "swift".
Here is the version I see most often. It compiles and looks reasonable.
// The common bug: every keystroke starts a request, none is cancelled.
func queryChanged(_ query: String) {
Task {
try? await Task.sleep(for: .milliseconds(300))
results = await client.search(query)
}
}The sleep delays every request. It cancels none of them. Five keystrokes still mean five searches, and whichever finishes last wins. The fix is to keep a handle on the previous task and cancel it.
private var searchTask: Task<Void, Never>?
func queryChanged(_ query: String) {
searchTask?.cancel()
searchTask = Task {
do {
try await Task.sleep(for: .milliseconds(300))
let found = try await client.search(query)
try Task.checkCancellation()
results = found
} catch {
// Cancelled or failed. A newer query owns the screen.
}
}
}Task.sleep throws when the task is cancelled, so a cancelled search never reaches the network. The checkCancellation after the request stops a slow, stale response from overwriting a newer one. In SwiftUI, .task(id: query) gives you the same cancellation for free, and saying so earns the point too.
What gets scored: knowing that debounce is cancellation plus a delay, not only a delay. Protecting against stale results. Handling the empty query without a request.
5. Fix the buggy snippet: read before you type
I paste 30 lines and say, "Something is wrong here." The usual suspects in iOS: a retain cycle through a stored closure, UI state touched from a background context, a missing [weak self] where the object really can outlive the screen, a force unwrap on data from the network, or a cell that keeps an old image.
What gets scored: reading the whole snippet before changing anything. Naming the cause, not the symptom. "This closure is stored on the object and captures self strongly, so neither is ever released" beats "add weak self" every time. Then the smallest fix, and one sentence on how you would catch it next time: a test, the memory graph, or the Swift 6 compiler checks.
The candidates who lose this task start editing in the first ten seconds. The ones who win say, "Give me a minute to read it through, I'll talk as I go."
How a 60-minute round actually runs
Formats vary, but the shape of a live iOS round is stable. Here is the clock I run, and where most candidates spend their time wrongly.
| Minutes | What happens | What you should be doing |
|---|---|---|
| 0 to 5 | Introductions, the task is shared | Listen. Do not open the editor yet. |
| 5 to 10 | Clarifying questions | Ask about data, errors, platform version, UIKit or SwiftUI. |
| 10 to 15 | Plan out loud | Name the types, the state, the order you will build in. |
| 15 to 40 | Build the core | Happy path first, running early, narrating each decision. |
| 40 to 50 | Edge cases and extensions | Errors, empty state, cancellation, or the follow-up I add. |
| 50 to 60 | Review and your questions | Say what you would change with more time. Ask one real question. |
The most common failure is skipping minutes 5 to 15. The candidate starts typing at minute 3, builds the wrong thing well, and spends the last 20 minutes undoing it. Five minutes of questions buy back twenty.
What to say at the moments that count
- When the task lands: "Before I start, can I check a few things about the data and what should happen on failure?"
- Before the first line: "Here is my plan. A model, a client behind a protocol, a view model with one state enum, then the view."
- When you take a shortcut: "I'm hardcoding this for now so we see it running. In production this comes from injection."
- When it first runs: "Happy path works. Next I'll handle errors and the empty list, unless you'd rather I go somewhere else."
- At the end: "With more time I'd add a test for the view model with a fake client, and cap the cache size."
Notice that none of these are clever. They show me your order of work, and they give me a chance to steer you before you spend ten minutes somewhere I do not care about.
Recovery lines when it breaks
Something will go wrong. A compiler error you do not recognise. A blank screen. A question you cannot answer. I am not scoring whether it breaks. I am scoring what you do in the next thirty seconds.
- Compiler error you do not understand: "Let me read the whole message. It says this isn't Sendable, so I'm passing a non-isolated type across actors. I'll check where."
- Screen shows nothing: "Let me check the three places it can fail: the request, the decoding, the state update. I'll add a print at each."
- You realise the design is wrong: "I've built this into a corner. I'd rather spend two minutes restructuring than patch it. Is that fine?"
- You forget an API name: "I don't remember the exact signature. In real work I'd check the docs. I'll write what I expect and we can fix the name."
- You run out of time: "I won't finish error handling. Here is exactly what I'd add and where it goes."
Every one of these tells me the same thing: you debug by narrowing, not by guessing. That is the skill I am hiring for.
A one-week iOS interview prep drill
- Open an empty project. Build the list screen in 30 minutes, talking out loud, recording yourself.
- Next day, decode a messy JSON payload with a date and a missing field. Say what fails when one item is bad.
- Build the image cache. Then write down the reentrancy problem in two sentences, from memory.
- Build debounced search twice: once with a stored task, once with
.task(id:). - Ask a friend for a buggy snippet, or take one from old code. Read it fully before touching it.
- Turn on the Swift 6 language mode and fix every warning in what you built this week.
- Watch your recordings. Count the silences longer than 30 seconds. Those are what the interviewer counted too.
If your loop also has a knowledge round, the next step is the 5 follow-ups that decide a senior iOS loop.
Questions engineers ask about the iOS coding interview
What kind of coding questions come up in an iOS interview?
Mostly small app tasks, not puzzles. A list screen that loads data, a JSON model, an image cache, a debounced search field, or a buggy snippet to fix. Some companies add one algorithm question, usually at the level of a dictionary or a set, solved in Swift.
Do I need to finish the task to pass an iOS coding test?
No. A working core with a clear plan for the rest usually scores better than a finished screen nobody could follow. What loses the round is silence, a crash you cannot explain, or code that ignores loading and error states.
How should I prepare for an iOS live coding interview?
Build the five common tasks from an empty project, on a timer, while talking out loud. Do each one twice: once in SwiftUI, once with the concurrency rules of Swift 6 turned on, so the compiler catches what the interviewer would.
