Senior iOS Interview Knowledge Recap
This page is for fast review, not a second answer library. Use Interview Paths to practice questions by role and difficulty; those questions link to the canonical technical and system-design answers.
🟪 Syntax · 🟦 Types/APIs · 🟩 Correct pattern · 🟥 Bug · 🟨 Gotcha · 🧠 Explanation
Quick Recap — Interview hit points
🧠 Use this answer order: definition → owner → iOS example → failure mode → tradeoff. Start with one or two sentences; expand only when asked.
Protocols in 30 seconds
| Prompt | Hit point |
|---|---|
| What is a protocol? | A contract for capabilities; a concrete type adopts and implements it. |
| Can you instantiate one? | No. Instantiate a conformer; store it as any P only when type erasure is useful. |
Can let or var hold a protocol? | Yes: let screen: any P = HomeScreen(); use var to replace it with another conformer. |
Generic or any P? | Generic preserves type relationships; any P enables runtime substitution and storage. |
some P or any P? | some hides one fixed type; any can hold different conformers at runtime. |
| Associated type? | A related type chosen by each conformance. |
| Primary associated type? | Lets callers constrain an important associated type, such as any Cache<Data>. |
Self requirement? | Refers to the exact conforming type and can limit existential operations. |
| Extension dispatch? | Through a protocol-typed value, requirements use the conformer's witness; extension-only members resolve statically. |
| Class-bound protocol? | Add AnyObject when reference identity or a weak delegate is required. |
| Protocol composition? | P & Q requires both capabilities without creating a new hierarchy. |
| Type erasure? | Wraps different generic implementations behind one stable type; use deliberately. |
Protocol plus Sendable? | Use when conformers must safely cross isolation domains. |
| Protocol for every type? | No. Add one at a real substitution, I/O, ownership, or testing boundary. |
Senior iOS in 60 seconds
- Architecture: dependencies point toward stable business rules; create them in a composition root.
- Ownership: state, tasks, subscriptions, navigation, and caches each need one clear owner.
- Models: DTOs mirror transport, domain models express meaning, and view state drives rendering.
- Concurrency: isolate mutable state, revalidate after
await, propagate cancellation, and prevent stale results. - SwiftUI: keep
bodydeclarative; state lifetime follows ownership and view identity. - UIKit: understand lifecycle, reuse, Auto Layout priorities, containment, and main-thread UI work.
- Networking: model errors, validate responses, retry only when safe, and support cancellation.
- Security: keep secrets out of logs, store tokens appropriately, and plan pin rotation before pinning.
- Performance: measure first; investigate launch, hangs, hitches, allocations, and unnecessary main-actor work.
- Persistence: plan migrations, offline behavior, conflict resolution, and recovery from partial failure.
- Testing: inject nondeterminism; test behavior, boundaries, cancellation, and failure paths without sleeps.
- Delivery: use observability, feature flags, staged rollout, and a tested rollback path.
- Leadership: state tradeoffs, make risks visible, document the decision, and help the team execute it.
Code-review checklist
- Who owns this state or task?
- What happens on cancellation, retry, timeout, or partial failure?
- Can an older async result overwrite newer state?
- Is mutable shared state isolated correctly?
- Is the abstraction useful, or is it protocol ceremony?
- Are UI updates, accessibility, and lifecycle behavior correct?
- Can the behavior be tested deterministically?
Avoid these interview traps
- Do not say one architecture is always best; tie the choice to requirements and change axes.
- Do not describe an actor as a background thread; it is an isolation mechanism.
- Do not claim
asyncautomatically runs work off the main actor. - Do not use
[weak self]automatically; explain the ownership and closure lifetime. - Do not add
@unchecked Sendablewithout proving synchronization. - Do not create a protocol only to say the code is testable.
Rapid answers
Can you instantiate a protocol?
No. A protocol defines requirements; instantiate a conforming concrete type, then store it through an existential if needed: let loader: any UserLoading = LiveUserLoader(). See 08-Swift-Types-Protocols-and-Generics.md and 13-Swift-Code-Quizzes.md for examples.
let versus var?
let makes the binding immutable; var allows reassignment. For structs, a let instance cannot have its stored var properties changed. For a class, let fixes the reference, but the referenced object can still mutate its own var properties.
What is the difference between @StateObject and @ObservedObject?
@StateObject creates and owns the observable object for the view's lifetime. @ObservedObject observes an object owned and injected by somebody else.
What is @Binding?
A binding is two-way access to value state owned elsewhere. It does not own the value and is not a Combine publisher.
@Published versus Publisher?
@Published is a property wrapper that emits changes for one property. Publisher is the general Combine stream protocol. $property exposes the publisher created by @Published.
What is AnyCancellable?
It is a type-erased cancellation token that keeps a Combine subscription alive while retained and cancels it when explicitly cancelled or released.
What is an actor?
An actor is a reference type that serializes access to its isolated mutable state. External access across the isolation boundary normally requires await.
What is Sendable?
It is a concurrency contract stating that a value can safely cross isolation boundaries. It is unrelated to JSON serialization despite often appearing on Codable value models.
Serial versus concurrent queue?
A serial queue runs one submitted block at a time. A concurrent queue may run multiple blocks simultaneously. The main queue is serial; global queues are concurrent.
What causes a GCD deadlock?
The classic case is synchronously dispatching to the same serial queue that is already executing the caller. Circular waits between queues or locks also deadlock.
Does ARC remove retain cycles?
No. ARC releases an instance only when its strong reference count reaches zero. A cycle keeps the count above zero until the cycle is broken.
Frame versus bounds?
Frame is the view's rectangle in its parent's coordinate system. Bounds is the view's own internal coordinate system.
OCP versus LSP?
OCP concerns adding implementations without modifying stable consumers. LSP requires each implementation to preserve the abstraction's behavioral promises.
Why separate DTO and domain models?
DTOs follow external schemas; domain models follow application meaning. Separation prevents API changes and transport concerns from spreading through business and UI code.
What does SSL pinning add?
It adds an application-controlled certificate or public-key check on top of normal TLS trust evaluation. It improves resistance to certain interception attacks but requires careful rotation and backup-pin planning.
Protocol interview deep dive
Protocol, generic, opaque, or existential?
| Form | What the compiler knows | Strong use case | Main tradeoff |
|---|---|---|---|
P in a conformance | A concrete type promises P's requirements | Adding a capability to a type | The conformance must satisfy the complete contract |
T: P | The concrete T stays known and related across the API | Reusable algorithms and compile-time specialization | Can make public signatures more complex |
some P | One fixed concrete type is hidden from the caller | Returning implementation detail, especially SwiftUI views | Every return path must resolve to the same underlying type |
any P | The value conforms to P, but its concrete type is erased | Stored dependencies and heterogeneous collections | Erases type relationships and can add indirection |
🧠 A protocol is the contract. T: P, some P, and any P are different ways to use that contract; they are not interchangeable spellings.
Why would you choose a generic instead of any Protocol?
Choose a generic when the concrete type relationship matters to the operation. The compiler retains associated-type information and can specialize the implementation. Choose an existential when runtime substitution and simple storage matter more than preserving the exact concrete type.
protocol ImageLoading<Image> {
associatedtype Image
func load() async throws -> Image
}
func loadTwice<Loader: ImageLoading>(
with loader: Loader
) async throws -> (Loader.Image, Loader.Image) {
async let first = loader.load()
async let second = loader.load()
return try await (first, second)
}The generic signature guarantees that both results have the same Loader.Image type. A plain, unconstrained existential would hide that relationship.
When is any Protocol the right dependency type?
Use it when a consumer needs only the protocol requirements and the implementation may be selected at runtime. A small dependency protocol can make ownership and test substitution explicit without exposing the concrete service.
import Foundation
struct User: Sendable {
let id: UUID
let name: String
}
protocol UserRepository: Sendable {
func user(id: UUID) async throws -> User
}
@MainActor
final class ProfileModel {
private let repository: any UserRepository
private(set) var user: User?
init(repository: any UserRepository) {
self.repository = repository
}
func load(id: UUID) async throws {
user = try await repository.user(id: id)
}
}🟨 Do not add a protocol only because mocking is possible. Add one when there is a useful boundary, more than one implementation, external I/O, nondeterminism, or a dependency whose behavior the consumer should not own.
What is an associated type?
An associated type is a placeholder chosen by each conforming type. It expresses a relationship among a protocol's requirements. It differs from a generic parameter because the conformer selects it for the conformance, not the caller for each function call.
protocol Store {
associatedtype Value
func save(_ value: Value) async throws
func load() async throws -> Value?
}
struct SettingsStore: Store {
func save(_ value: Settings) async throws { }
func load() async throws -> Settings? { nil }
}
struct Settings: Codable, Sendable {
var isDarkModeEnabled: Bool
}What does a primary associated type add?
It lets clients name a commonly constrained associated type in angle brackets. It improves generic constraints and constrained existential storage; it does not turn the protocol itself into a nominal generic type.
import Foundation
protocol Cache<Value> {
associatedtype Value
func value(for key: String) -> Value?
}
func readAvatar(
from cache: any Cache<Data>,
key: String
) -> Data? {
cache.value(for: key)
}Why does Self sometimes make existential use difficult?
Self means the exact conforming type. A requirement that accepts or returns Self preserves a relationship the existential may erase, so not every such operation can be called meaningfully on any P. Keep the consumer generic, expose a constrained associated type, or add deliberate type erasure.
protocol Mergeable {
func merged(with other: Self) -> Self
}
func merge<T: Mergeable>(_ first: T, _ second: T) -> T {
first.merged(with: second)
}How does protocol-extension dispatch work?
If a method is a protocol requirement, a conformer's implementation is used through the protocol witness table. If it exists only in an extension, a call made through an existential or generic protocol constraint resolves to the extension implementation known at that static call site.
protocol StatusReporting {
func status() -> String
}
extension StatusReporting {
func status() -> String { "default" }
}
struct LiveReporter: StatusReporting {
func status() -> String { "live" }
}
let reporter: any StatusReporting = LiveReporter()
print(reporter.status()) // "live" because status is a requirement🟨 If status() were removed from the protocol declaration and existed only in the extension, the existential call would use the extension implementation. This is a frequent senior-level dispatch question.
Why make delegate protocols class-bound?
A delegate is commonly held weakly to avoid a retain cycle. AnyObject restricts conformers to reference types, which makes weak storage possible.
@MainActor
protocol ProfileCoordinatorDelegate: AnyObject {
func profileCoordinatorDidFinish()
}
@MainActor
final class ProfileCoordinator {
weak var delegate: (any ProfileCoordinatorDelegate)?
func finish() {
delegate?.profileCoordinatorDidFinish()
}
}Protocol composition versus protocol inheritance?
Protocol inheritance defines a new refined contract. Composition with & describes a use-site value that must satisfy several existing contracts.
protocol Logging {
func log(_ message: String)
}
protocol MetricsRecording {
func increment(_ name: String)
}
func recordLaunch(using client: any Logging & MetricsRecording) {
client.log("Application launched")
client.increment("launch")
}When is type erasure useful?
Type erasure is useful when an API must store different generic implementations behind one stable type while preserving a selected operation. It costs boilerplate and can hide capabilities, so prefer a constrained existential when it expresses the requirement directly.
Can a protocol require stored properties or optional requirements?
A protocol requires property access, not storage. A conformer may satisfy { get } or { get set } with stored or computed properties. Native Swift protocol requirements are not optional. @objc optional is limited to Objective-C-compatible, class-only designs; in pure Swift, split capabilities into smaller protocols or provide an intentional default implementation.
When are default implementations dangerous?
Defaults are helpful for universal behavior and reusable algorithms. They are dangerous when they silently satisfy important business requirements, create surprising dispatch, or make a new requirement compile without every conformer making an explicit decision.
Does protocol-oriented programming mean “create a protocol for every type”?
No. Protocol-oriented design uses behavior-focused abstractions where substitutability or generic algorithms provide value. Concrete types are clearer when there is one stable implementation and no meaningful abstraction boundary.
Protocol code-review exercises
Try each prompt before opening the answer section.
Prompt 1: Can this dependency be constructed?
protocol PaymentProcessing {
func charge(cents: Int) async throws
}
let processor = PaymentProcessing()Prompt 2: Why can the struct not conform?
protocol Resettable {
func reset()
}
struct RetryPolicy: Resettable {
private(set) var attempts = 3
func reset() {
attempts = 0
}
}Prompt 3: Which implementation is printed?
protocol Describable { }
extension Describable {
func describe() -> String { "extension" }
}
struct Screen: Describable {
func describe() -> String { "screen" }
}
let item: any Describable = Screen()
print(item.describe())Prompt 4: What concurrency decision is missing from the design discussion?
protocol TokenReading {
func token() async -> String?
}
actor APIClient {
let reader: any TokenReading
init(reader: any TokenReading) {
self.reader = reader
}
}🟩 Protocol exercise answers
🟥 Answer 1: A protocol has no initializer
Construct a concrete conformer and optionally store it as an existential.
protocol PaymentProcessing {
func charge(cents: Int) async throws
}
struct LivePaymentProcessor: PaymentProcessing {
func charge(cents: Int) async throws { }
}
let processor: any PaymentProcessing = LivePaymentProcessor()🟥 Answer 2: The requirement and implementation must be mutating
A struct method that changes self or one of its stored properties must be mutating. The protocol must permit value-type conformers to mutate too.
protocol Resettable {
mutating func reset()
}
struct RetryPolicy: Resettable {
private(set) var attempts = 3
mutating func reset() {
attempts = 0
}
}🟨 Answer 3: It prints extension
describe() is not a protocol requirement, so the call through any Describable uses the extension-only member selected from the static type. Declare it as a requirement when conformers are expected to customize behavior.
protocol Describable {
func describe() -> String
}With that requirement, Screen supplies the witness and the same call prints screen.
🟨 Answer 4: Decide whether the dependency crosses isolation domains
An actor can own a non-Sendable dependency when the value is safely transferred and not shared elsewhere. If the same reader is intended to cross or be shared across isolation domains, make that contract explicit with Sendable and ensure conformers honor it.
protocol TokenReading: Sendable {
func token() async -> String?
}Do not add @unchecked Sendable just to silence the compiler. Use value semantics, actor isolation, locking, or another real synchronization strategy first.
Senior iOS question bank
Swift language and API design
- Explain value semantics, copy-on-write, and reference identity with an example from an app.
- When would you use an enum with associated values instead of a protocol hierarchy?
- Compare
some P,any P, andT: Pin a public API. - How do primary associated types improve an API without making the protocol generic?
- What is the difference between
SendableandCodable? - How do escaping closures affect ownership and actor isolation?
- When is
@unchecked Sendablelegitimate, and what proof must accompany it? - How would you evolve a public Swift package API without breaking source compatibility?
Architecture and modularity
- Draw the dependency direction for UI, domain, data, and platform layers.
- Which boundaries deserve protocols, and which should stay concrete?
- Where is the composition root, and who owns long-lived dependencies?
- How do you stop a “core” module from becoming a dumping ground?
- How would you split a large feature into packages without creating cyclic dependencies?
- How do DTOs, domain models, and view state differ in lifetime and responsibility?
- Describe an incremental migration from UIKit MVC to a testable architecture.
- How do deep links and navigation state cross feature-module boundaries?
Concurrency and reliability
- What can change after an
await, and how do you revalidate actor state? - Compare structured tasks, unstructured
Task, andTask.detached. - How do you propagate cancellation through networking, parsing, and UI state?
- Design single-flight token refresh without duplicate requests.
- How would you prevent an old search response from replacing a newer result?
- What does priority inversion look like, and how can it appear with locks or tasks?
- How do you bridge a callback API to async/await safely with continuations?
- When is an actor insufficient because an operation must be atomic across
await?
SwiftUI, UIKit, and lifecycle
- Who owns a SwiftUI model, and how do Observation and bindings expose it?
- Why is doing work in
bodyunsafe, and where should side effects live? - How do view identity and list identifiers affect state preservation and animation?
- How do you cancel image loading when a reusable cell or view disappears?
- Explain Auto Layout priorities and diagnose an ambiguous or unsatisfiable layout.
- How would you embed SwiftUI inside UIKit while preserving navigation ownership?
- How do you model restoration for a multi-step flow?
- Which accessibility behaviors must a custom control implement?
Performance, persistence, and networking
- How do you investigate a slow launch using measurements rather than guesses?
- What causes scrolling hitches, and which work belongs off the main actor?
- How do memory graphs, allocations, and time profiling answer different questions?
- When would you cache decoded models, raw responses, or rendered images?
- How do HTTP caching, ETags, retries, idempotency, and backoff interact?
- How do you migrate a persistent store while preserving user data and rollback options?
- What is your offline conflict strategy: last-write-wins, merge, or user resolution?
- How do you avoid decoding a very large response into multiple temporary copies?
Testing, delivery, and leadership
- What belongs in unit, integration, UI, snapshot, and performance tests?
- How do you make time, randomness, UUID generation, and network behavior deterministic?
- How would you test cancellation and actor-isolated state without timing sleeps?
- What makes a test double a fake, stub, spy, or mock, and when does the distinction help?
- How do feature flags support rollout and rollback without becoming permanent debt?
- Which metrics and logs would prove a release is healthy without exposing user data?
- How do you review an architectural proposal and surface tradeoffs constructively?
- Describe a production incident you owned: detection, mitigation, root cause, and prevention.
Senior code discussion: stale-result protection
An interviewer may ask you to review a search implementation. State the invariant first: only the latest query may update visible results.
import Foundation
@MainActor
final class SearchModel {
private let search: @Sendable (String) async throws -> [String]
private var searchTask: Task<Void, Never>?
private(set) var results: [String] = []
private(set) var errorMessage: String?
init(search: @escaping @Sendable (String) async throws -> [String]) {
self.search = search
}
func submit(query: String) {
searchTask?.cancel()
guard !query.isEmpty else {
results = []
errorMessage = nil
return
}
searchTask = Task {
do {
try await Task.sleep(for: .milliseconds(300))
let newResults = try await search(query)
try Task.checkCancellation()
results = newResults
errorMessage = nil
} catch is CancellationError {
// Expected when a newer query replaces this one.
} catch {
guard !Task.isCancelled else { return }
errorMessage = "Search failed"
}
}
}
deinit {
searchTask?.cancel()
}
}A strong discussion covers task ownership, cancellation, debounce behavior, stale results, error policy, main-actor UI state, and deterministic tests with an injected clock or controllable search closure.
Architecture questions to rehearse
- Design a modular iOS application used by multiple clients or tenants.
- Where do dependency creation and object lifetimes belong?
- How do you stop a shared/core module from becoming a dumping ground?
- Explain your choice between MVVM-C, VIPER, and a simpler MVVM design.
- Where do DTO mapping, validation, formatting, and navigation belong?
- How would you migrate a tightly coupled UIKit feature incrementally?
- How do you test coordinators, presenters, repositories, and mappers?
Strong answer characteristics:
- Starts from requirements and change axes
- Defines ownership and dependency direction
- Describes failure, cancellation, and threading
- Explains tradeoffs and incremental adoption
- Includes observability and testing
SwiftUI/Combine questions to rehearse
- Explain state ownership through a parent and two child views.
- Why can recreating an observed object in
bodybe a bug? - How would you model loading, empty, content, and error states?
- How do
subscribe(on:)andreceive(on:)differ? - How do you prevent stale search responses?
- Show a Combine retain cycle and fix it.
- When would you prefer async/await over Combine?
Good comparison:
Async/await is usually clearer for one-shot request/response workflows. Combine remains useful for long-lived or composed streams such as search text, connectivity, notifications, and multiple changing inputs.
Concurrency questions to rehearse
- Explain actor reentrancy and why assumptions can change after
await. - Build a single-flight token refresh mechanism.
- Protect a cache using an actor and then using GCD barriers.
- Explain cancellation propagation in structured concurrency.
- Compare Task priority with GCD QoS.
- Identify a main-thread deadlock from code.
- Explain when a value should conform to
Sendable.
Security questions to rehearse
- Where do access and refresh tokens belong?
- How do you prevent simultaneous refresh requests?
- What happens after refresh failure?
- Certificate versus public-key pinning?
- How do you rotate pins safely?
- What should never appear in logs or analytics?
- How do you separate authentication errors from business errors?
Practical coding challenges
API list
Implement:
- Codable DTO
- Repository protocol
- Async or Combine client
- Explicit view state
- Main-actor state update
- Error and retry UI
- Cancellation
- Unit tests with a fake repository
Debounced search
Implement:
- Query state
- Debounce and duplicate removal
- Empty-query handling
- Cancellation/switching to latest request
- Loading and error states
- Deterministic scheduler or async clock tests
Pagination
Implement:
isLoadingguard- cursor/page state
- end-of-list detection
- deduplication
- reset for a new query
- retry without corrupting the current page
Thread-safe store
Implement twice:
- Actor-based store
- Concurrent queue with barrier writes
Explain the consistency and completion semantics of each version.
Final reminders
- Say who owns each piece of state.
- Say which layer performs each transformation.
- Mention cancellation and errors in asynchronous designs.
- Avoid claiming that a pattern is always best.
- Prefer domain-specific examples from your own applications.
- If asked for code, first state the intended contract and edge cases.
