Bryn Flow

🍎 iOS Interview Questions & Answers

The questions that come up again and again in iOS developer interviews, with concise, correct answers — grouped by difficulty.

← iOS Track

Beginner Fundamentals

What is the difference between a struct and a class in Swift?

Structs are value types — assigning or passing one copies its data. Classes are reference types — assigning or passing one shares the same instance, so mutations through one reference are visible through another. Structs also can't inherit from another struct and have no deinitializer, since Swift manages their memory automatically without reference counting.

What is an optional, and why does Swift require you to unwrap it?

An optional (Type?) is a value that may be present or nil. Swift forces explicit unwrapping (if let, guard let, ??) so that the possibility of a missing value is visible in the code and handled deliberately, eliminating a whole class of null-pointer-style crashes at compile time.

What is the difference between @State and @Binding in SwiftUI?

@State declares a view's own local, mutable data — the view owns the source of truth. @Binding is a reference to a value owned by another view (usually a parent), passed down with a $ prefix, letting a child both read and write it.

What does the Codable protocol do?

It combines Encodable and Decodable, letting Swift automatically convert a type to and from formats like JSON without hand-written parsing code, as long as the type's structure maps cleanly to the data (or you supply custom CodingKeys/init logic when it doesn't).

What is the difference between UIKit and SwiftUI at a high level?

UIKit is imperative — you create view instances and mutate their properties directly, typically via view controllers. SwiftUI is declarative — you describe what the UI should look like for a given state, and the framework figures out how to update the actual rendered views when that state changes.

What does ARC (Automatic Reference Counting) do?

ARC automatically tracks how many strong references point to each class instance and deallocates it once that count reaches zero, freeing developers from manual memory management. It only applies to reference types (classes), not value types (structs/enums).

Intermediate Architecture & State

What is a retain cycle, and how do you break one?

A retain cycle happens when two class instances hold strong references to each other (or a closure captures self strongly while being stored as a property on self), so neither's reference count ever reaches zero and neither is deallocated. Break it by marking one side weak or unowned — commonly [weak self] in a closure that outlives the immediate call, like a network completion handler stored on a property.

When would you use @StateObject vs @ObservedObject?

@StateObject is for the view that creates and owns an ObservableObject — SwiftUI guarantees it's only instantiated once for the view's lifetime, even across recompositions. @ObservedObject is for a view that receives an already-created object from a parent; using it to create the object yourself risks the object being recreated on every parent recomposition, losing state.

What does the @MainActor attribute do?

It marks a type, property, or function as isolated to the main actor, meaning calls to it are guaranteed to run on the main thread. It's commonly applied to view models whose @Published properties drive UI updates, since SwiftUI requires those changes to happen on the main thread.

What is the difference between @FetchRequest and manually fetching from Core Data?

@FetchRequest is a SwiftUI property wrapper that subscribes to a Core Data query and automatically re-renders the view when the underlying store changes (inserts, updates, deletes). A manual fetch via context.fetch(request) gets a one-time snapshot — you'd need to observe NSManagedObjectContextObjectsDidChange yourself to react to later changes.

What is the purpose of a guard statement, and how is it different from if?

guard requires an early exit (return, break, continue, or throw) in its else branch, and any values it unwraps remain available for the rest of the enclosing scope. This makes it well suited for validating preconditions at the top of a function, keeping the "happy path" unindented, unlike an equivalent if let whose unwrapped value is only visible inside the if-block.

How does Task differ from DispatchQueue.global().async for background work?

Task is part of Swift's structured concurrency — it integrates with async/await, supports cooperative cancellation propagated automatically to child tasks, and its priority can be inferred from context. DispatchQueue.global().async is GCD's older, unstructured API — it has no built-in cancellation or async/await integration and requires manual completion handlers to return results.

Advanced Deep Dives

How does SwiftUI decide which parts of the view tree to re-render?

SwiftUI diffs the view's body output against the previous render using each view's identity (structural identity by type/position, or explicit identity via .id()) and equality of its inputs. When a @State, @StateObject, or @ObservedObject value a view depends on changes, SwiftUI invalidates and re-evaluates that view's body, then diffs the resulting view tree to compute the minimal set of underlying platform view updates — it doesn't naively redraw the whole screen.

What's the difference between weak and unowned references?

Both avoid creating a strong reference cycle. A weak reference automatically becomes nil when the referenced instance is deallocated, so it must be declared as an optional. An unowned reference assumes the referenced instance will always outlive it and is not optional — accessing it after the instance is deallocated is a runtime crash, so it should only be used when that lifetime guarantee genuinely holds (e.g. a child that never outlives its parent).

How would you avoid a duplicate network request when a SwiftUI view using .task re-renders?

.task is tied to the view's identity, not every recomposition — SwiftUI only restarts it if the view is recreated with a new identity (e.g. its id changes) or removed and re-added to the tree. A plain body recomputation from unrelated state changes does not restart it. If duplicate requests still occur because the id genuinely changes, guard with an @State flag or cache the in-flight Task and reuse it.

How does Core Data handle concurrency across multiple contexts?

Each NSManagedObjectContext is confined to the queue it was created with (main queue or a private queue) — objects fetched from one context must not be touched directly from another thread. Multi-context setups typically use a main-queue viewContext for UI and a private background context (often a child of the persistent container) for writes, syncing via NSManagedObjectID (which is thread-safe) and calling context.perform { } to hop onto the right queue.

What is the difference between @Published property changes and objectWillChange.send()?

@Published automatically calls objectWillChange.send() right before its wrapped value changes, notifying SwiftUI to re-render observing views. Calling objectWillChange.send() manually is useful when a change needs to be signaled that isn't captured by a single @Published property — e.g. mutating a nested reference type's internal state, or batching several property changes into one notification.

Keep practicing

Take the iOS practice quiz or walk through a full project tutorial.

Practice Quiz