iOS · 16 min read

Senior iOS interview questions, and what the follow-up tests

Twelve senior iOS interview questions with what each one scores, a weak answer, a senior answer and the follow-up. Swift concurrency, SwiftUI, memory, architecture.

By Mike Salari · Staff mobile engineer · 500+ technical interviews from the hiring side. · Published

Most lists of senior iOS interview questions are lists of definitions. What is an actor, what is @StateObject, what is ARC. A senior candidate can answer every one of them and still get a “no hire”, because the senior round is not scored on the first answer. It is scored on what happens when the interviewer pushes on it.

This guide covers twelve questions that come up in senior iOS loops, grouped by the round they belong to. For each one: what the interviewer is actually scoring, what a weak answer sounds like, what a senior answer sounds like, and the follow-up that separates the two. The questions are the kind asked in real loops; none is attributed to a company, because loops change and I will not claim a list I cannot verify.

How the senior round is scored

Before the questions, the rubric. When I write up a senior iOS candidate, the notes fall into four columns, and every question below is really a probe for one or more of them.

Signal What I write down What a miss looks like
Signal The first answer says something only a person who has shipped this would say A correct definition with no production detail
Structure I can follow the reasoning without reconstructing it Jumping between three ideas, none finished
Depth The answer holds at the second and third question The answer collapses to “it depends” at the first push
Judgment When I change the constraint, the answer changes, and the candidate says why The same answer, repeated louder

A correct answer that scores on none of these four is a mid-level answer. That is the whole difference, and it is why memorising more definitions does not move a senior loop.

Swift and concurrency

1. “What does @MainActor on a class actually guarantee?”

What is scored: whether you understand isolation as a property of code, not of the work it waits for.

Weak answer: “Everything in the class runs on the main thread.”

Senior answer: “Every method and property of the class is isolated to the main actor, so synchronous code in it runs on the main thread and the compiler stops other isolation domains from touching its state directly. It does not mean the work it awaits runs on the main thread. If a method awaits URLSession.data(from:), the method suspends, the main actor is free while the request is in flight, and the method resumes on the main actor when the result comes back. The thing to watch is heavy synchronous work inside the class, like decoding a large JSON payload, which does block the main thread.”

The follow-up: “So where would you decode that payload?” A senior candidate moves the decode into a nonisolated function or a separate type and explains that the decoded value must be Sendable to cross back. A mid-level candidate says “in a background thread” and cannot say how that interacts with the actor.

@MainActor
final class ProfileModel {
    private(set) var profile: Profile?
    private var loadTask: Task<Void, Never>?

    func load(id: Int) {
        loadTask?.cancel()
        loadTask = Task {
            do {
                let url = URL(string: "https://example.com/profiles/\(id)")!
                let (data, _) = try await URLSession.shared.data(from: url)
                try Task.checkCancellation()
                profile = try JSONDecoder().decode(Profile.self, from: data)
            } catch is CancellationError {
                // A newer load replaced this one.
            } catch {
                profile = nil
            }
        }
    }
}

This type-checks in Swift 6 language mode (checked with Swift 6.3.2). Notice what an interviewer will ask about it: the decode still runs on the main actor, the previous task is cancelled but cancellation is cooperative, and checkCancellation() after the await is what stops a stale response from overwriting a newer one.

2. “What is actor reentrancy, and when has it bitten you?”

What is scored: whether you have debugged it, not whether you can define it.

Weak answer: “Actors can be re-entered while awaiting.” Correct, and nothing else.

Senior answer: “An actor method that awaits gives up the actor while it is suspended, so another call can run and change the actor’s state before the first one resumes. The classic bug is a cache: two callers ask for the same image, both check the dictionary, both miss, both start a download. The fix is to store the in-flight task, not only the result, so the second caller awaits the first caller’s task.”

actor ImageCache {
    private var images: [URL: Data] = [:]
    private var inFlight: [URL: Task<Data, Error>] = [:]

    func data(for url: URL) async throws -> Data {
        if let cached = images[url] { return cached }
        if let running = inFlight[url] { return try await running.value }
        let task = Task { try await URLSession.shared.data(from: url).0 }
        inFlight[url] = task
        defer { inFlight[url] = nil }
        let data = try await task.value
        images[url] = data
        return data
    }
}

The follow-up: “What happens to the first caller if the second caller’s view disappears?” The shared task is not cancelled when one waiter goes away, which is usually what you want for a cache and occasionally not. A senior candidate names that trade-off without being asked twice.

3. “What does Sendable protect you from, and when do you reach for @unchecked Sendable?”

What is scored: judgement about escape hatches.

Weak answer: “Sendable means thread-safe. I add @unchecked Sendable when the compiler complains.”

Senior answer: “Sendable marks types that are safe to pass between isolation domains: value types made of Sendable parts, actors, and final classes with only immutable Sendable state. In Swift 6 language mode the compiler enforces it. @unchecked Sendable tells the compiler to trust me, so I use it only when the type protects its own state with a lock or a serial queue, and I leave a comment saying which one. If I find myself adding it to make a warning go away, the design is usually wrong: the state should live in an actor.”

The follow-up: “You are migrating a large codebase to Swift 6. Where do you start?” Staff-level answers talk about sequencing: turning on complete checking module by module, starting with leaf modules, and not letting @unchecked become the migration strategy.

SwiftUI and state

4. “Why is this view re-rendering, and how would you prove it?”

What is scored: whether you measure before changing anything.

Weak answer: “Because the state changed. I would add EquatableView or split the view.”

Senior answer: “First I would confirm it. Self._printChanges() inside body during development prints which property triggered the evaluation, and the SwiftUI template in Instruments shows how often bodies are evaluated. The usual causes are an ObservableObject publishing on every change when the view only reads one property, or state held too high in the tree so a change at the top re-evaluates everything below it. With the Observation framework on iOS 17 and later, a view only depends on the properties it actually reads in body, which removes the first cause.”

The follow-up: “The app supports iOS 16. Now what?” The answer changes: without Observation, you split the object, move state down to the view that owns it, or pass only the values a child needs. That change of answer when the constraint changes is exactly the judgement column.

5. “@State, @StateObject, @ObservedObject: who owns the object?”

What is scored: ownership, which is the real question behind the three wrappers.

Weak answer: a definition of each wrapper.

Senior answer: “The question is who creates the object and how long it should live. @StateObject means this view owns it and SwiftUI keeps it for the view’s lifetime in the tree, even when the view struct is recreated. @ObservedObject means someone else owns it and passes it in. If you create an object with @ObservedObject inside a view, it is recreated whenever the parent re-evaluates, and you lose its state: that is the bug I have seen most. With @Observable models the same rule holds with @State as the owning wrapper.”

The follow-up: “Where does view identity come in?” If the view’s identity changes, because of an if branch or a changing id, SwiftUI treats it as a new view and the owned state is discarded. A senior candidate connects state lifetime to identity without prompting.

6. “When would you still use UIKit?”

What is scored: whether your answer comes from shipped work or from opinion.

Senior answer: specific cases with reasons: a text editor with fine-grained control of selection and layout, a collection view with complex compositional layouts and diffable data sources that already works, a screen where you measured SwiftUI’s cost and it was too high on older devices. Then the other side: new screens in SwiftUI by default, and UIViewRepresentable or UIHostingController at the boundary. A candidate who says “never” or “always” loses the judgement column immediately.

Memory and performance

7. “How do you find a retain cycle?”

Weak answer: “Use [weak self] everywhere.”

Senior answer: “I look for the symptom first: a view controller’s deinit never runs, or memory grows each time a screen is opened and closed. The Memory Graph Debugger in Xcode shows the cycle directly. The usual culprits are closures stored by an object they capture strongly, delegates declared strong, and timers or notification observers that retain their target. I use [weak self] where the closure is stored and can outlive the object, not everywhere, because a weak capture in a short-lived closure only adds optional handling.”

The follow-up: “Does a Task inside a view model create a cycle?” A Task captures self strongly until it finishes. That is not a permanent cycle, but a long-running or infinite loop inside it keeps the object alive. The fix is to keep the task handle and cancel it when the owner goes away.

8. “The feed scrolls badly on a three-year-old iPhone. Walk me through it.”

What is scored: method. This is the question where I learn most about a candidate.

Senior answer, in order: reproduce it on the device, not the simulator. Profile with Time Profiler and the Animation Hitches instrument. Look for work on the main thread during scroll: image decoding, text layout, synchronous disk reads, Auto Layout churn in cells. Fix the biggest one first and measure again. For images specifically: decode off the main thread (UIImage.byPreparingForDisplay() on iOS 15 and later), downsample to the size shown, and cache by memory cost rather than item count, for example NSCache with totalCostLimit.

The changed constraint: “Memory warnings start after two minutes of scrolling.” Now the cache is the suspect, and the answer shifts from speed to eviction: cost-based limits, clearing on memory warning, and not holding full-size images for thumbnails.

Architecture

9. “Why did you choose that architecture?”

What is scored: whether you can argue for a decision including its costs.

Weak answer: “MVVM is the standard.” Or a tour of every pattern ever written.

Senior answer: a decision tied to the team and the app: how many engineers, how many features, what needs to be tested, where the boundaries are. Then the cost: “MVVM with coordinators gave us testable view models and navigation out of the views, and it cost us boilerplate for small screens. For those we let the view own simple state.” Naming what the choice costs is the senior signal. More in iOS architecture interview questions.

10. “How would you modularise this app for five teams?”

What is scored: Staff-level thinking about ownership.

Strong answer: feature modules owned by teams, a small set of shared core modules with strict dependency direction (features depend on core, never on each other), interfaces in separate modules so features can depend on an interface without its implementation, and build times as a measured outcome. Then the organisational part: who owns the core modules, and how a change to them is reviewed. At Staff level the second half matters more than the first.

System design and delivery

11. “Design offline support for a notes app.”

This is the entry point to the design round, covered properly in iOS system design interview. What a senior answer contains in the first two minutes: the local store is the source of truth for the UI, writes go to the local store first and into an outbox, a sync process sends the outbox when the network returns, and conflicts have a written rule. The follow-up is always about conflicts: “The user edits the same note on two devices while offline.” If your answer has no rule for that, the round is lost there.

12. “A release crashed for 2% of users. What do you do in the first hour?”

What is scored: operational judgement, which senior interviews test more than most candidates expect.

Senior answer: stop the damage first: halt the phased release in App Store Connect, turn off the feature flag if the crash is behind one. Then read the crash reports for the common stack and device pattern. Decide between a server-side fix, a flag, or an expedited build, and say how you would communicate it. Then the durable part: what check would have caught it before release. A candidate who starts with “I would debug the code” has skipped the step the business cares about.

The follow-ups that decide the loop

Across all twelve, the pattern of follow-ups is the same, and you can prepare for the pattern rather than for each question.

Follow-up type Example What a strong answer does
Change the constraint “Now support iOS 16.” Changes the answer and says why
Scale it “Now five teams share it.” Moves from code to ownership
Break it “The process is killed mid-upload.” Names the failure and the recovery
Measure it “How do you know it is faster?” Names the tool and the number
Cost it “What does that choice cost you?” Names a real cost, not “nothing”

How to use this list

Do not read the senior answers and nod. Cover each one, say your own answer out loud, then ask yourself the follow-up in the table above and answer that too. The gap between the first answer and the second is what the interview measures.

Sources

Practise the follow-up, not only the first answer

Interview Runtime asks the question, scores your answer against a written rubric, and then asks the follow-up. iOS and Android only. See Interview Runtime.