iOS interviews test the Swift type system, how ARC manages memory, and whether you can reason about UI state — plus at least one question where a retain cycle is hiding in plain sight. Below are the questions asked most often, each with a model answer. Junior: core theory, definitions, and simple practical cases.
1
What problem do optionals solve, and how do you unwrap one safely?
Answer
An optional makes "this value may be absent" part of the type, so the compiler forces you to handle the empty case instead of crashing at runtime the way a null pointer would. Safe unwrapping means if let or guard let for a value you need in scope, nil-coalescing (??) for a default, and optional chaining when you only care about the happy path. Force unwrapping with ! is a promise to the compiler that the value exists — acceptable for outlets and programmer errors, never for network data.
2
What is the difference between a struct and a class in Swift? Which do you default to?
Answer
Structs are value types: assigning one copies it, they cannot inherit, and they are stack-allocated when possible. Classes are reference types with identity, inheritance, deinit, and ARC overhead. The Swift convention is to start with a struct and reach for a class only when you need reference semantics — shared mutable state, an Objective-C interoperability requirement, or a lifecycle you must observe.
3
How does ARC work, and what causes a retain cycle?
Answer
ARC tracks how many strong references point at an object and deallocates it when the count hits zero — it is compile-time inserted retain/release calls, not a runtime garbage collector. A retain cycle happens when two objects hold strong references to each other, so neither count ever reaches zero: a classic case is a view controller owning a closure that captures self strongly. You break it with [weak self] or [unowned self] in the capture list.
4
When do you use weak versus unowned?
Answer
Both avoid incrementing the retain count. weak makes the reference optional and sets it to nil when the object is deallocated, so it is safe if the other object may die first — delegates are the standard example. unowned is non-optional and assumes the referenced object outlives you; if that assumption breaks, the app crashes. Use unowned only when the lifetime relationship is genuinely guaranteed, and weak whenever you are unsure.
5
What is the difference between @State, @Binding, @StateObject and @ObservedObject?
Answer
@State is local value-type state owned by the view; SwiftUI stores it outside the struct so it survives re-renders. @Binding is a read-write reference to state owned by someone else, which is how a child mutates a parent's value. @StateObject creates and owns a reference-type observable object and is initialised exactly once. @ObservedObject observes an object owned elsewhere — using it where you needed @StateObject recreates the object on every re-render and silently resets its state.
6
How does SwiftUI decide to re-render a view?
Answer
A view is a lightweight struct describing the UI; SwiftUI rebuilds its body whenever a dependency it read changes, then diffs the result against the previous tree and applies only the differences to the underlying render. Dependencies are the property wrappers a view actually reads, plus the Environment. Performance problems almost always come from state placed too high in the tree, so an unrelated change invalidates a large subtree.
7
What does async/await change compared to completion handlers and GCD?
Answer
async/await lets asynchronous code read top to bottom, so error handling uses ordinary try/catch and there is no "pyramid of doom" or forgotten completion path. The compiler enforces that awaits happen in an async context, and structured concurrency ties child task lifetimes to their parent, so cancellation propagates. GCD is still relevant for legacy code and for fine-grained queue control, but new code should default to async/await, with Combine reserved for genuine streams of values over time.
8
What is an escaping closure and why does the keyword exist?
Answer
A closure is escaping when it is stored and called after the function returns — a network completion handler, for example. Swift requires the @escaping annotation because a non-escaping closure can be optimised more aggressively and cannot capture self strongly by accident. The annotation is also the signal to think about capture semantics: escaping closures are where retain cycles are born.
9
Walk through the UIViewController lifecycle. Where do you put layout code?
Answer
viewDidLoad runs once after the view hierarchy loads and is where you do one-time setup. viewWillAppear and viewDidAppear run on every presentation and are for work that must repeat, like refreshing data or starting an animation. Layout that depends on final frame sizes belongs in viewDidLayoutSubviews, because in viewDidLoad the bounds are not yet final — putting frame maths there is a very common bug that only shows on a different screen size.
🦎
Reading answers is not enough
In a real interview you speak under pressure. Cam asks these same questions, scores every answer, and shows exactly what to fix.
Practice a Junior iOS (Swift) interview →Free · 3 interviews per month