Senior / Staff iOS — interview prep
Swift language
value semantics · optionals · protocols · ARC · closures · errors · wrappers · COW · Sendable
struct vs classOptionalsProtocolsARCClosuresthrows / ResultProperty wrappersCOWSendableKey paths
01
Core mental model — types & semantics
Q1Struct / enum
- Value types — assignment copies (often COW under the hood)
- Compared with
== - No inheritance; protocol conformance only
- Default choice for models and pure data
Class
- Reference type — shared identity,
=== - ARC + retain cycles with delegates / closures
- UIKit / Obj-C interop, subclassing
Actor
- Reference type with isolated mutable state
- Compiler serializes access — not a general “model” replacement
- Pair with async/await at boundaries
Struct vs class — shape
// Value type — copy-on-write friendly, no inheritance
struct Profile: Equatable {
var displayName: String
}
// Reference type — identity, ARC, subclassing
final class Session {
var token: String
init(token: String) { self.token = token }
}
Actor — isolated mutable state
actor Counter {
private var value = 0
func next() -> Int {
value += 1
return value
}
}
i
Interview sound bite: Prefer structs for value semantics and thread-friendly copies; use classes for shared mutable identity and framework patterns; use actors for concurrent shared mutable resources — not for every view model.
02
Optionals — safety without pyramid code
Q2guard, chaining, coalescing
func title(for user: User?) -> String {
guard let user else { return "Guest" }
return user.profile?.displayName?.trimmingCharacters(in: .whitespaces)
?? "Unknown"
}
// map / flatMap — avoid nested if let
let id = optionalString.flatMap(Int.init)
guard let
- Nil means exit — invalid state or early return
- Keeps the rest of the scope flat
if let
- Both branches matter — optional UI or features
- Avoid deep nesting; consider map/flatMap
Avoid !
- Force-unwrap crashes on nil
- Reserve for invariants (document why) or fail-fast dev-only paths
03
Protocols & generics
Q3Associated types + constrained extension
protocol Identifiable {
associatedtype ID: Hashable
var id: ID { get }
}
extension Array where Element: Identifiable {
func index(of element: Element) -> Int? {
firstIndex { $0.id == element.id }
}
}
Generics
whereclauses refine requirements- Type erased at call site — concrete types at compile time
- Prefer generics over
Anywhen possible
Existentials
any Protocol— boxed, dynamic dispatch- Some protocols with associated types need
anyor generics
04
ARC — strong, weak, unowned
Q4weak self in escaping closures
class DetailVC: UIViewController {
var onDismiss: (() -> Void)?
deinit { print("DetailVC gone") }
func wire() {
network.fetch { [weak self] result in
guard let self else { return }
self.apply(result)
}
}
}
// unowned — only when lifetime is provably nested (expert use)
// parent holds child; child never outlives parent
strong
- Default — keeps object alive
- Delegate / parent refs often need weak to avoid cycles
weak
- Optional — becomes nil when deallocated
- Use for delegates, callbacks, child → parent
unowned
- Non-optional — dangling pointer if wrong
- Only when lifetime is provably nested (expert-only)
05
Closures
Q5@escaping vs synchronous use
func load(completion: @escaping (Result<Data, Error>) -> Void) {
URLSession.shared.dataTask(with: url) { data, _, err in
// completion invoked later → must be @escaping
completion(.success(data ?? Data()))
}.resume()
}
func map(_ x: Int, _ f: (Int) -> Int) -> Int { f(x) } // f is non-escaping
@escaping
- Closure may outlive the function — stored or async
- Self captures need
[weak self]vigilance
Trailing closure
- Last closure arg can trail outside parens
- Multiple trailing closures (SwiftUI-style) — label clarity matters
06
Errors — throws & Result
Q6throws + Result
enum LoadError: Error { case offline, badData }
func load() throws -> String {
guard isOnline else { throw LoadError.offline }
return try parse(raw)
}
// Result — async boundaries, Combine, callbacks
let r: Result<User, Error> = .failure(LoadError.badData)
throws
- Typed errors with enums conforming to Error
- Callers use try / try? / try!
Result
- Completion handlers and async boundaries
- map / flatMap without throwing through the whole stack
07
Property wrappers
Q7@propertyWrapper + projection
@propertyWrapper
struct Clamped {
private var value: Int
let range: ClosedRange<Int>
init(wrappedValue: Int, _ range: ClosedRange<Int>) {
self.range = range
self.value = min(max(wrappedValue, range.lowerBound), range.upperBound)
}
var wrappedValue: Int {
get { value }
set { value = min(max(newValue, range.lowerBound), range.upperBound) }
}
}
struct Form {
@Clamped(0...100) var progress = 0
// $progress — projected value (wrapper-specific)
}
Common built-ins
@State,@Binding,@Published— know what each projects$name— projected value (wrapper-specific)
Interview angle
- Wrappers synthesize boilerplate (get/set, observation)
- Custom wrappers: init, wrappedValue, sometimes projectedValue
08
Copy-on-write & collections
Q8Array — shared buffer until mutation
var a = [1, 2, 3]
var b = a // shares buffer until one mutates
b.append(4) // copy now if needed
// Element type matters: [UIImage] copies references, not bitmaps
!
COW caveat: Collections copy cheaply, but elements that are reference types still share instances — mutating one element’s class state affects all “copies” holding that reference.
09
Sendable & isolation (language level)
Q9@Sendable + key paths
// Sendable — cross-actor / concurrency boundaries
func runLater(_ work: @Sendable @escaping () -> Void) { }
struct Person: Sendable { let name: String; let age: Int }
let kp = \Person.age
people.sorted { $0[keyPath: kp] < $1[keyPath: kp] }
Sendable
- Marks types safe to share across concurrency domains
- Closures crossing actors often need @Sendable
mutating
- Struct methods that mutate self
- Value semantics — each mutation conceptually affects that binding
10
Quick comparison tables
Q1–Q10| Topic | Default / rule of thumb | Gotcha |
|---|---|---|
| struct vs class | struct for value data; class for identity | COW collections don’t fix shared class instances |
| Optional | guard for early exit; ?? for defaults | Optional chaining stops at first nil |
| weak vs unowned | default to weak | unowned if you’re wrong → crash |
| @escaping | anything stored or called later | capture list to break cycles |