iOS · 16 min
Senior iOS interview questions for 2026, and the follow-ups
Twelve senior iOS interview questions for 2026: what each scores, a weak and a senior answer, and the follow-up on Swift 6 concurrency, SwiftUI, architecture.
iOS · 14 min read
Ten iOS architecture interview questions and how senior answers are scored: MVVM, TCA, navigation, dependency injection, SPM modules, testing.
The architecture round is the one where candidates most often talk themselves out of a senior offer by being right about the wrong thing. They explain MVVM correctly, compare it to VIPER correctly, and never say why they would choose either for the app in front of them. The interviewer is not testing whether you know the patterns. They are testing whether you can make a decision, defend it, and say what it costs.
| Signal | Strong | Weak |
|---|---|---|
| Decision | “For this app and this team, I would choose X” | A tour of every pattern |
| Reasons | Tied to team size, testing needs, the app’s complexity | “It’s the industry standard” |
| Cost | Names what the choice makes harder | “No downsides” |
| Change | Knows when they would change their mind | Defends the choice against every new constraint |
This is how I think about it, and what I listen for in a candidate’s reasoning. It is a starting point, not a rule.
| Situation | Reasonable default | What it costs |
|---|---|---|
| One or two engineers, a few screens | SwiftUI views with @Observable models; little ceremony |
Logic can drift into views as the app grows |
| A team of five to ten, many screens | MVVM with injected dependencies, navigation owned outside the views | Boilerplate for trivial screens |
| Several teams, one app | Feature modules in Swift packages, each with a public interface; a composition root in the app target | Build setup, interface discipline, slower first builds |
| Complex shared state, strong testing culture | A unidirectional architecture such as TCA | Learning curve, dependency on a third-party library, verbosity |
A candidate who produces something like this unprompted, with their own reasons, is scoring on every column at once.
Weak: “MVVM, because it separates concerns.”
Senior: asks a few questions first (team size, how long the app will live, what needs testing), then chooses and names the cost. “Eight engineers, a five-year-old app moving to SwiftUI: MVVM with @Observable view models, dependencies injected through initialisers, navigation in a router per feature. It costs us a view model for screens that barely need one; we accept that for consistency.”
Senior: initialiser injection of protocols (or closures for single functions), with one composition root that builds the object graph. A framework becomes worth it when the graph is large enough that wiring by hand is the bottleneck, and not before.
struct Order: Equatable, Sendable { let id: String }
protocol OrderLoading: Sendable {
func orders() async throws -> [Order]
}
@MainActor
final class OrdersViewModel {
enum State: Equatable { case idle, loading, loaded([Order]), failed }
private(set) var state: State = .idle
private let loader: any OrderLoading
init(loader: any OrderLoading) { self.loader = loader }
func refresh() async {
state = .loading
do {
let orders = try await loader.orders()
state = .loaded(orders)
} catch {
state = .failed
}
}
}A test passes a stub that conforms to OrderLoading and checks the state. A test like this checks that the state moves from loading to loaded with the stub’s orders, and to failed when the stub throws. The follow-up: “Protocol or closure?” A closure is lighter for one function; a protocol is clearer when there are several related functions or when the dependency has state. Either answer is fine with a reason.
Senior: not the view. In UIKit, coordinators or a router per flow. In SwiftUI, NavigationStack with a path owned by a router object or the feature’s model, so deep links and tests can set the path directly. The follow-up: “A push notification must open a screen three levels deep.” With the path in a model, that is setting an array. With navigation scattered across views, it is a week.
The second follow-up: “The link arrives while the user is logged out.” Parsing the URL is the easy part. The senior answer adds a planner between the link and the router: given the link and the current session, it decides the whole stack to build. Logged out means login first, with the target remembered, then the target. An id the app cannot resolve gets a safe fallback screen instead of a half-built stack. The detail that separates levels is what a route holds: an id and small values, not the model. Put a live Transaction in the route and state restoration either fails to encode it or brings back the amount as it was when the path was saved. An id is looked up fresh on every restore. And since the planner is a plain function returning a value, each of those cases is a unit test with no view on screen.
enum Route: Codable, Equatable {
case signIn
case orders
case order(id: String)
case unavailable
}
enum LinkTarget: Equatable {
case order(id: String)
}
struct RoutePlan: Equatable {
let path: [Route]
let deferredTarget: LinkTarget?
}
func planRoute(
target: LinkTarget?,
signedIn: Bool,
availableOrderIDs: Set<String>
) -> RoutePlan {
guard signedIn else {
return RoutePlan(path: [.signIn], deferredTarget: target)
}
guard let target else {
return RoutePlan(path: [.orders], deferredTarget: nil)
}
switch target {
case .order(let id):
guard availableOrderIDs.contains(id) else {
return RoutePlan(path: [.orders, .unavailable], deferredTarget: nil)
}
return RoutePlan(path: [.orders, .order(id: id)], deferredTarget: nil)
}
}After sign-in, call the planner again with current facts. After decoding a restored path, check again before showing signed-in content. availableOrderIDs is only an input here; the real check goes through the service, and an id in a route never grants access.
Senior: Swift packages per feature, a small set of core packages (networking, design system, analytics), strict dependency direction so features never import each other, and interface packages when one feature needs another’s capability. Then the measured reason: build time and ownership. Staff: who owns the core packages and how a breaking change to them is rolled out across teams.
Senior: not in views, and not in the networking layer. In view models for screen logic, and in a domain type or service when two screens share the rule. Then the test: “If I delete the UI, can I still test this rule?”
What is scored: whether you can evaluate a third-party architecture fairly.
Senior: what it gives (a single state per feature, explicit effects, strong testing tools), what it costs (learning curve for new hires, a large dependency, verbosity for simple screens, being tied to the library’s release cycle), and the condition under which you would adopt it: a team that values the testing model and has someone who knows it well. “It is over-engineered” and “it is the best” both lose points.
Senior: each layer translates errors into its own vocabulary. The network layer knows HTTP status codes; the repository turns them into domain errors such as notFound or offline; the view model maps those to what the user sees. The UI never shows an URLError description. The follow-up: “Which errors do you retry?” Transient ones (timeouts, 5xx), with backoff, and a 429 after the delay the server asks for. Other 4xx, never. A timeout on a request that changes state may have succeeded on the server, so retry it only with an idempotency key.
Senior: unit tests on view models and domain logic with stubbed dependencies, a small number of integration tests on the data layer with a real in-memory store, and UI tests only for the critical paths because they are slow and flaky. Snapshot tests for the design system if the team maintains one. The ratio is a decision, and a senior candidate says what they would not test.
What is scored: Staff-level judgement.
Senior or Staff: ask what problem the rewrite solves and how it would be measured. Propose an incremental path instead: new features in the new pattern, old screens migrated when touched, a boundary between the two. A full rewrite is justified only when the current structure blocks work the business needs, and even then in stages. Say which path writes the state at each step: old and new writing the same data is a risk, and a flag that hides the new screen does not stop its stored data or background work.
What is scored: honesty and reflection. The best answers name a real decision that aged badly, what the signal was, and what it would take to change now. “Nothing” is the only wrong answer.
“What does that cost you?” If you prepare only one thing for the architecture round, prepare a cost for every choice you make. The Android version of this round is in Android architecture interview questions; the design round it feeds into is in iOS system design interview.

iOS · 16 min
Twelve senior iOS interview questions for 2026: what each scores, a weak and a senior answer, and the follow-up on Swift 6 concurrency, SwiftUI, architecture.

Mobile system design · 15 min
What an iOS design round adds: background limits, background URLSession, storage, image pipelines and push, with a worked photo feed design.

Android · 14 min
Ten Android architecture interview questions and how senior answers are scored: layers, the domain layer, UDF, Hilt, modules, navigation and testing.