12.7 — 100+ Interview Questions, Organized
Every answer below comes in three levels: Junior (correct surface), Mid (mechanism), Senior (tradeoffs, recovery, and “I’d also consider…”). The 3-level system is explained in 12.8. For now: read the level matching your target role; one level up tells you what the next role expects.
Swift Language (20)
1. What is an optional?
Junior: A type that can hold a value or nil; Optional<T> is an enum with .some(T) and .none.
Mid: Optionals make absence explicit at the type level; unwrap via if let, guard let, ??, optional chaining, or ! (force-unwrap, only when invariants guarantee non-nil).
Senior: Optionals are Swift’s correctness lever — they push nullability into the type system so the compiler enforces handling. I avoid force-unwrap except at integration boundaries where the invariant is provable (e.g., URL(string: "https://known.com")!). At API boundaries I prefer Result<T, E> or throwing functions over T? because they carry error information.
2. struct vs class?
J: struct is a value type, copied on assignment; class is a reference type, shared by reference.
M: Structs get free Equatable/Hashable synthesis if all stored properties conform; classes participate in inheritance and reference identity; structs are stack-allocated when possible.
S: Default to struct. Reach for class only when (a) identity matters (a Window controller), (b) inheritance is required, (c) ARC lifecycle is needed (deinit hooks), or (d) you want shared mutable state (rare and intentional). Swift’s COW (copy-on-write) on Array/Dictionary means struct copies are cheap; performance is rarely a reason to choose class.
3. What is Codable?
J: Type alias for Encodable & Decodable; conforming types can be serialized to/from JSON, plist, etc.
M: Synthesis works when properties are themselves Codable. Customize with CodingKeys, init(from:), encode(to:). Use JSONDecoder.dateDecodingStrategy, keyDecodingStrategy = .convertFromSnakeCase, etc.
S: I match server payload to Swift idioms via CodingKeys and keyDecodingStrategy rather than corrupting domain types with snake_case. For polymorphic JSON I use a type discriminator + a custom init(from:). For schema migration I keep separate DTO types and map to domain models — it isolates wire-format change from app code.
4. ARC and retain cycles?
J: ARC counts references; when count hits zero, the object deallocates. A retain cycle is two objects strongly referencing each other so neither can deallocate.
M: Break cycles with weak (optional, becomes nil when target deallocs) or unowned (non-optional, crashes if accessed after dealloc). Closures capture self strongly by default — use [weak self] to break.
S: I run Instruments’ Leaks/Allocations on every feature before shipping. Common cycles: delegate properties that should be weak, closures stored on self capturing self, NSTimer or DispatchSourceTimer holding their target. For closures whose lifetime is bounded by the function call, no capture list is needed; for long-lived stored closures, [weak self] is the default with explicit unwrap inside.
5. weak vs unowned?
J: Both prevent strong references; weak is optional (becomes nil), unowned is non-optional.
M: Use unowned when the lifetime guarantees the reference outlives the use; weak when the target may deallocate first.
S: Default to weak — the cost is one optional unwrap, the upside is crash safety. I use unowned only when (a) the relationship is parent-owns-child and the child has a back-reference, and (b) the child’s lifetime is strictly bounded by the parent’s. In Swift 5.7+ I prefer [weak self] in guard let self else { return } over unowned self to avoid even the theoretical crash.
6. Generics: when and why?
J: Generics let you write code parameterized by type, like Array<Element>.
M: Constrain with where T: Equatable or T: SomeProtocol. The compiler monomorphizes — generates specialized code per type — so there’s no runtime cost.
S: I reach for generics to express relationships between types (a Repository<Element>), not just to avoid duplication. For polymorphic dispatch I prefer existentials (any Protocol) at boundaries and generics inside hot code. Since Swift 5.7, opaque return types (some Protocol) often replace generic parameters at the API surface for cleaner signatures.
7. Protocols with associated types (PATs)?
J: Protocols that have associatedtype Element so conformers specify the concrete type.
M: PATs can’t be used as existentials before Swift 5.7. Since then any Collection works but has performance overhead; some Collection (opaque) avoids the overhead.
S: PATs model abstract algorithms (Sequence, Collection, Identifiable’s ID). The mid 2020s rule: use some PAT for opaque returns when the caller doesn’t need the concrete type; any PAT when you need to store heterogeneous conformers; generics for full type-parameter freedom. The right choice depends on whether the concrete type leaks into the API contract.
8. Equatable and Hashable — synthesized when?
J: For structs/enums where all stored properties are Equatable/Hashable; just declare conformance.
M: Classes don’t synthesize — provide == and hash(into:) manually. Synthesis respects access level.
S: I synthesize where safe; manually implement == when only a subset of properties define identity (e.g., Account equality should compare id only, not cachedAvatar). For caching/sets, Hashable must agree with Equatable — combining only the identity field in both.
9. Error handling: throws vs Result?
J: throws propagates errors up the call stack; Result<Success, Failure> carries the outcome as a value.
M: throws integrates with try/catch and async; Result is useful for callbacks or storing errors for later.
S: For Swift Concurrency code, throws is idiomatic. Result shines when you need to defer error handling — TCA effects, batch results, multi-error collection. I avoid mixing both styles in a single API; pick one and stick with it. Define typed errors (enum NetworkError) instead of leaking any Error so callers can reason about handling.
10. async/await vs Combine?
J: async/await writes async code linearly; Combine models event streams as Publishers.
M: async shines for one-shot operations; Combine for streams (UI events, polling). They interop via .values (publisher → AsyncSequence) and Future/Subject.
S: I’d lean fully on async/await for new code, with AsyncStream/AsyncSequence for streams. Combine is great but Apple’s investment in it has slowed; AsyncStream is the future. Bridging code (NotificationCenter, KVO) still benefits from Combine wrappers, but new architectures should be concurrency-first.
11. Actors?
J: A reference type that serializes access to its mutable state.
M: Methods on actors are implicitly async from outside; only one task can be inside the actor at a time. @MainActor ensures execution on the main thread.
S: Actors solve data races by construction. The catch: reentrancy — an await inside an actor method can let another task in. Hold invariants across awaits explicitly. For UI state, @MainActor is the right tool; for shared mutable caches, a custom actor. Pure value types passed across actor boundaries must be Sendable.
12. Sendable?
J: A marker protocol for types safe to send across concurrency domains.
M: Value types whose components are Sendable get conformance synthesized. Reference types need to be immutable or explicitly synchronized; mark them @unchecked Sendable if you’ve manually verified safety.
S: Swift 6 strict concurrency makes Sendable violations into compile errors. The migration cost is real but the payoff is the elimination of an entire class of data-race bugs. For legacy types I can’t easily make Sendable, I isolate them inside an actor or wrap them in @MainActor until I can refactor.
13. Property wrappers?
J: Syntax (@State, @Published) that lets a type augment a property’s storage and access.
M: Defined with @propertyWrapper struct; provides wrappedValue, optional projectedValue (the $ syntax).
S: Useful for cross-cutting concerns (persistence with @AppStorage, validation, threading). But every wrapper hides behavior at the use site — I use them sparingly, with names that hint at the cost (@MainActor-aware, lazy, etc.). For business logic, plain methods communicate intent better.
14. KeyPaths?
J: A type-safe reference to a property: \Person.name has type KeyPath<Person, String>.
M: Used by SwiftUI’s \Person.name-style API, sort(by:) with key paths, dynamic member lookup.
S: KeyPaths enable powerful generic APIs without runtime reflection — they’re compile-time. I lean on them for sorting, filtering, and DSL-style APIs. Combined with @dynamicMemberLookup, they enable Swift’s modern reactive frameworks (Observation, SwiftUI bindings).
15. Result builders (function builders)?
J: The DSL machinery behind SwiftUI’s body { … } and RegexBuilder.
M: An @resultBuilder type provides buildBlock, buildOptional, buildEither, etc. that compose nested expressions.
S: Powerful for declarative DSLs but easy to abuse — error messages from misuse are notoriously bad. I’d use them for stable APIs where the DSL benefit (readability) clearly outweighs the diagnostic cost.
16. Existentials (any P)?
J: A type-erased box that holds any conformer of protocol P.
M: Pre-Swift 5.7, written P directly (now warned). Since 5.7, requires any P. Has dynamic-dispatch overhead vs generics.
S: I use existentials at API boundaries (heterogeneous collections of conformers); generics inside hot loops. For PATs, existentials were impossible before 5.7 — Apple now allows them but the runtime cost is real. The compiler hint to write any P is teaching the cost.
17. lazy properties?
J: A stored property computed on first access, not at init.
M: Only var can be lazy; not thread-safe (multiple threads may race the first access).
S: Useful for expensive initialization; dangerous in concurrent contexts. For thread safety I’d wrap with an actor or use dispatch_once-style synchronization. In actors, lazy is implicitly safe because actor methods serialize access.
18. final keyword?
J: Prevents a class from being subclassed or a method from being overridden.
M: Enables compiler optimization (devirtualization). For modules with library evolution off (apps), defaults to non-final; with library evolution on (frameworks), defaults to final.
S: I mark every class final by default. Inheritance is a design choice, not a default; the compiler optimizations are a bonus. For SwiftUI/UIKit subclasses (UIViewController subclasses) final is fine because we don’t subclass our own subclasses.
19. Memory ordering / inout?
J: inout lets a function mutate its caller’s variable. Caller writes &value.
M: inout uses copy-in-copy-out semantics; the function operates on a local copy and writes back on return. Be careful with aliasing rules (a single inout can’t alias another argument).
S: For performance-sensitive code, inout avoids retain/release on classes. The Law of Exclusivity (Swift 5+) statically prevents most aliasing bugs. For batch mutations, prefer inout over reassigning a class reference for clarity.
20. Macros (Swift 5.9+)?
J: Compile-time code generation, like #Preview, @Observable, @Model.
M: Two kinds: freestanding (#fn(args)) and attached (@Macro). Implemented as SwiftPM packages that the compiler invokes during build.
S: Macros eliminate boilerplate without runtime overhead. The downsides: longer compile times, harder debugging (need to expand the macro to see generated code), and platform/version constraints. I trust Apple’s macros (@Observable, @Model); third-party macros I evaluate carefully for project longevity.
UIKit (15)
21. View controller lifecycle?
J: loadView → viewDidLoad → viewWillAppear → viewWillLayoutSubviews → viewDidLayoutSubviews → viewDidAppear. Mirror on disappear.
M: viewDidLoad runs once. viewWillAppear runs every show (including back navigation). Layout passes can run multiple times.
S: Don’t do animations in viewDidAppear — too late, view’s already visible. Don’t fetch data in viewWillAppear unless you want it to refire on every navigation back. Heavy work in viewDidLoad blocks first paint — defer to a Task. Memory pressure: implement didReceiveMemoryWarning to clear non-critical caches.
22. Auto Layout vs frame-based layout?
J: Auto Layout uses constraints (relationships between views); frame-based sets explicit positions.
M: Auto Layout adapts to dynamic content, multiple screen sizes, RTL languages. Frame-based is faster but brittle.
S: Modern UIKit is Auto Layout by default. Frame-based survives in custom collection view layouts and gesture-driven animations where performance is critical. For static, dense layouts I sometimes drop to manual layout in layoutSubviews. Always anchor to safeAreaLayoutGuide not view for top/bottom constraints.
23. UITableView vs UICollectionView?
J: TableView is single-column lists; CollectionView is flexible (grids, custom layouts).
M: Both support cell reuse via dequeueReusableCell. CollectionView has UICollectionViewLayout for custom arrangements; iOS 13+ added UICollectionViewCompositionalLayout.
S: I default to CollectionView with Compositional Layout in new code — it does everything TableView does plus more, with better APIs (diffable data source). TableView remains for true table semantics (Settings-style screens with insets and sections). For very large lists, profile cell configuration time; complex cells benefit from prefetch and async image loading.
24. Diffable data source?
J: API (iOS 13+) where you describe the data state via snapshots; UIKit diffs and animates.
M: UICollectionViewDiffableDataSource<Section, Item>; items must be Hashable; snapshots are applied atomically.
S: Diffable replaces the error-prone performBatchUpdates dance. Gotchas: item identity must be stable (use a stable ID hash, not the entire object); reconfiguring vs reloading items has different animation behavior. For performance, reuse cell registrations via UICollectionView.CellRegistration.
25. Auto Layout performance tips?
J: Avoid too many constraints; reuse views via dequeue.
M: Profile with Instruments’ “Layout” tool. Common culprits: setting translatesAutoresizingMaskIntoConstraints wrong, conflicting priorities, unnecessary setNeedsLayout calls.
S: For dense lists (chat apps, feeds), self-sizing cells with Auto Layout can hit ~50 layout passes per scroll second. Optimizations: cache cell heights with a heuristic, use UICollectionViewCompositionalLayout’s estimated sizes carefully, or for extreme cases drop to manual layout in sizeThatFits and layoutSubviews.
26. Delegate pattern?
J: An object holds a weak reference to a delegate (protocol-conformer) and calls back into it.
M: Always weak to avoid retain cycles. Protocol can be class-bound to allow weak, or use AnyObject constraint.
S: Delegate is the idiomatic UIKit callback pattern, but it doesn’t scale to many observers — for that use NotificationCenter or Combine. For multi-callback APIs, consider protocols with default implementations to make adoption cheap. In modern Swift, replacing delegates with AsyncStream works well for event sequences.
27. Responder chain?
J: Touch events flow through view hierarchy: UIView → superview → … → UIViewController → UIApplication.
M: First responder handles text input. becomeFirstResponder()/resignFirstResponder() control focus. Custom actions via UIResponder.canPerformAction.
S: The responder chain is iOS’s secret event router — useful for cross-cutting actions (a “save” toolbar button that finds the right responder). Custom UIKit components benefit from participating in the chain via overriding next and canPerformAction. SwiftUI hides this; bridging back via UIViewControllerRepresentable exposes it again.
28. Threading: main vs background?
J: UIKit must be touched from the main thread; long work on background queues.
M: DispatchQueue.main.async, DispatchQueue.global(qos:), OperationQueue. Wrap UI updates: DispatchQueue.main.async { self.label.text = … }.
S: Concurrency in 2026 means @MainActor annotation for UIKit-touching code; Task.detached for background work. The Main Thread Checker catches violations in debug. Profile with Instruments’ Time Profiler to find work blocking the main thread — common culprits: JSONDecoder of large payloads, image decoding, Auto Layout passes.
29. viewWillTransition(to:with:)?
J: Called on orientation changes or split-screen resize.
M: coordinator.animate(alongsideTransition:) lets you animate alongside.
S: For iPad multitasking, this fires often. Use traitCollectionDidChange for richer adaptation (size class changes). For SwiftUI, this is replaced by @Environment(\.horizontalSizeClass) and GeometryReader.
30. UIView animations vs Core Animation?
J: UIView.animate(withDuration:) is a high-level wrapper; under the hood, Core Animation animates CALayer properties.
M: Properties animatable: position, bounds, transform, opacity, backgroundColor, etc. CA-only properties (cornerRadius, custom layers) need explicit CABasicAnimation.
S: For physics-y feel, UIViewPropertyAnimator allows interruptible, reversible animations. For 60+fps custom animations, use CADisplayLink driving manual CATransform3D updates. For Metal-backed effects, CAMetalLayer. Knowing when each layer of the abstraction is needed is the senior skill.
31. CALayer performance traps?
J: cornerRadius + masksToBounds triggers offscreen rendering, hurting scroll performance.
M: Profile in Instruments → Core Animation → toggle “Color Offscreen-Rendered Yellow”. Pre-rasterize with shouldRasterize = true (set rasterizationScale).
S: Modern iOS hardware handles corner rounding cheaply for most cases, but stacked translucent layers, shadows, and complex masks still tank scroll FPS. Pre-rendering rounded images at the image-loading layer beats per-frame corner masking. For lists with many shadows, use a static shadowPath instead of automatic shadow computation.
32. UIScrollView contentOffset vs contentInset?
J: contentOffset is the scroll position; contentInset adds padding inside the scroll bounds.
M: adjustedContentInset includes safe area + keyboard. iOS 11+ added contentInsetAdjustmentBehavior for fine control.
S: Mishandled insets are the #1 source of “content under nav bar” bugs. Always read adjustedContentInset, not raw contentInset. For keyboard avoidance, observe UIResponder.keyboardWillShowNotification and adjust contentInset.bottom. SwiftUI’s .scrollDismissesKeyboard removes much of this pain.
33. Storyboards vs programmatic UI?
J: Storyboards are visual editors; programmatic UI is code-only. M: Storyboards merge poorly in git, can be slow to render in Xcode, and segue lifecycle is opaque. Programmatic gives full control. S: For teams larger than two engineers, programmatic UI (or SwiftUI) wins. For solo prototypes, Storyboards are fast. XIBs (single-view) are a middle ground — used for individual cells/views. Apple’s own samples have been increasingly programmatic since iOS 14.
34. UICollectionViewCompositionalLayout?
J: A flexible CollectionView layout system: define groups of items into sections.
M: Compose NSCollectionLayoutItem into NSCollectionLayoutGroup into NSCollectionLayoutSection. Supports orthogonal scrolling per section.
S: This is the right layout for 2026 — replaces UICollectionViewFlowLayout for any non-trivial grid. Pairs with diffable data sources for declarative collection screens. Performance is excellent; the API is verbose but the readability win is real once you’ve internalized the composition model.
35. UIKit + SwiftUI interop?
J: UIHostingController wraps a SwiftUI view in UIKit; UIViewRepresentable wraps UIKit in SwiftUI.
M: UIViewControllerRepresentable for full VC bridging. Bindings flow via @Binding for two-way state.
S: Bridge at the largest sensible boundary — a whole screen, not individual labels. For navigation, wrap SwiftUI in UIHostingController inside an existing UINavigationController. The performance overhead of hosting is small; the code complexity overhead of mixing is what to manage.
SwiftUI (15)
36. @State vs @StateObject vs @ObservedObject vs @Bindable?
J: @State for value-type local state; @StateObject for reference-type state owned by this view; @ObservedObject for reference-type state injected from outside; @Bindable (iOS 17+) for @Observable macro types.
M: @StateObject initializes once per view lifetime; @ObservedObject re-initializes when the parent re-creates the view (a common bug source).
S: With @Observable macro (Swift 5.9+), the world simplified: @State for owned reference types, @Environment(MyType.self) for injected. The Combine-era distinctions are mostly legacy. For new code I default to @Observable + @State/@Environment and avoid ObservableObject entirely.
37. View identity in SwiftUI?
J: Identity is what makes SwiftUI know “this is the same view, animate the change.”
M: Position-based by default; .id(value) makes identity explicit. Changing identity tears down and rebuilds the view.
S: Identity bugs are SwiftUI’s hardest — a view “loses its state” because identity changed (often via .id(UUID()) in body, fatal). Use .id() only for intentional resets. For ForEach, the identifier must be stable across data updates. Misunderstanding identity is the most common cause of SwiftUI animation glitches.
38. body is called how often?
J: Whenever the view’s dependencies change.
M: SwiftUI tracks property reads inside body. Only reads of @State/@Observable properties trigger re-evaluation; computed properties not stored as state don’t.
S: Treat body as a pure function called frequently — never put side effects in it. For expensive computation, cache in @State or move out via .task modifier. The @Observable macro is more granular than ObservableObject: it tracks per-property reads, so unrelated properties don’t trigger redraws.
39. NavigationStack vs NavigationView?
J: NavigationView is the iOS 13 API; NavigationStack (iOS 16+) is the modern replacement.
M: NavigationStack exposes the navigation path as state (@State var path: [Route]), enabling deep linking, programmatic navigation, and proper SwiftUI-style declarative navigation.
S: NavigationStack’s biggest win is deterministic state-driven navigation. Build a Route enum, bind a NavigationPath (or typed array), and navigation becomes testable. Avoid mixing NavigationLink(isActive:) legacy API with NavigationStack — they conflict.
40. task vs onAppear?
J: task runs async work; onAppear is a sync callback.
M: task auto-cancels when the view disappears. onAppear doesn’t, so async work started there can leak past view lifetime.
S: Default to .task for any async work — the auto-cancellation is critical. Use .task(id:) to restart work when an ID changes. onAppear is for sync analytics/logging only. The .refreshable modifier ties into the same cooperative cancellation model.
41. EnvironmentObject vs @Environment(MyType.self)?
J: Both inject shared values down the view tree.
M: EnvironmentObject is the Combine-era API for ObservableObject; @Environment(MyType.self) is the iOS 17+ API for @Observable.
S: For new code, use @Observable + @Environment(MyType.self). Missing environment objects crash at runtime in the legacy API — a footgun. The new API also crashes if missing, but with @Observable you can provide defaults via @Bindable patterns more easily.
42. GeometryReader — when and when not?
J: A view that reads its parent’s size.
M: Use to read available space; pitfall is it claims all available space, breaking layouts that expect natural sizing.
S: I avoid GeometryReader for layout — use stack alignment and frame modifiers first. Reach for it only when I need coordinate transforms (drag offsets) or precise read-back of size for animations. iOS 16+ ViewThatFits and Grid removed most legitimate uses.
43. PreferenceKey?
J: A way for child views to send values up to ancestors.
M: Define a PreferenceKey with defaultValue and reduce; children set values via .preference(key:value:); ancestor reads via .onPreferenceChange(_:perform:).
S: Useful for “tell my parent how tall I am” patterns and for cross-cutting concerns like collecting all visible items in a scroll view. Overuse leads to upward data flow that fights SwiftUI’s declarative grain — use sparingly, prefer state at the right level instead.
44. View modifiers — when to extract?
J: When you copy the same chain of modifiers more than twice, extract into a ViewModifier.
M: struct MyStyle: ViewModifier { func body(content: Content) -> some View { … } }, then .modifier(MyStyle()) or .myStyle() via extension.
S: Custom modifiers are the right abstraction for cross-cutting visual concerns (a design-system “card” style). Don’t wrap them in functions that return some View just to save a few characters — actual ViewModifier types compose better and participate in identity properly.
45. Animations: implicit vs explicit?
J: .animation(.default, value: state) is implicit; withAnimation { … } is explicit.
M: Implicit attaches to a specific value change; explicit wraps the state mutation. Animation modifier order matters — modifiers after .animation aren’t animated.
S: I prefer explicit withAnimation because the cause-effect link is at the call site. The iOS 17+ .animation(_:value:) form with a closure is even cleaner. For complex sequenced animations, Animation.spring(response:dampingFraction:) + withAnimation + Task.sleep patterns are more readable than nested completion handlers.
46. Transactions?
J: A Transaction carries animation context through a state change.
M: withTransaction(Transaction(animation: nil)) { state = newValue } disables animation for one mutation; transaction { $0.disablesAnimations = true } for view-level control.
S: Useful when child animations should override parent context — e.g., disabling animation for a scroll position restore inside an otherwise-animated parent. Power user feature; most apps never need it explicitly.
47. @Bindable (iOS 17+)?
J: Lets you create bindings ($model.name) into an @Observable reference type that you don’t own.
M: Replaces the @ObservedObject + $ pattern. For state owned by the view, @State already provides bindings.
S: The @Bindable macro fills the gap where you need two-way bindings into a reference type passed down. Avoid overusing — if a child needs to mutate a parent’s state, often a callback (onCommit:) is clearer than a binding.
48. SwiftUI performance debugging?
J: Use Self._printChanges() inside body to see what triggered re-evaluation.
M: Instruments has a “SwiftUI” template tracking view body evaluations and update frequency. Equatable views can short-circuit comparisons.
S: For 60fps lists, the typical wins: make rows Equatable, hoist filtering out of body, avoid GeometryReader in row cells, use LazyVStack/LazyHStack over eager stacks for long lists. The @Observable macro is more performant than @Published because it tracks per-property reads.
49. List vs LazyVStack?
J: List provides built-in styling (separators, swipe actions); LazyVStack is a stack that only renders visible children.
M: List lazily renders rows too. LazyVStack is bare — you provide everything. List uses UITableView under the hood (iOS), so it inherits some UIKit quirks.
S: List for system-conforming list UI; LazyVStack inside ScrollView for custom layouts (chat, feeds with mixed cell heights). For very large lists in LazyVStack, give items stable IDs via ForEach(items, id: \.id) to enable diffing.
50. Cross-platform with SwiftUI?
J: One codebase targets iOS, macOS, watchOS, tvOS, visionOS.
M: Conditional code via #if os(iOS); size classes adapt layouts. Some modifiers are platform-specific.
S: True cross-platform shares ~70 % of UI code; the rest is adaptation. Build with .modifier(PlatformSpecificStyle()) patterns to keep platform ifs contained. macOS often needs custom keyboard handling, window scenes, and different navigation conventions. visionOS adds spatial considerations. Apple’s Backyard Birds sample is a good reference.
Data & Networking (15)
51. URLSession configuration types?
J: .default, .ephemeral (no disk caching), .background (downloads survive app suspension).
M: Background sessions require a URLSessionDelegate and the app’s handleEventsForBackgroundURLSession.
S: Most apps use .default with a custom URLSessionConfiguration setting timeoutIntervalForRequest, httpAdditionalHeaders, and urlCache. Background sessions are for large file ops (video downloads, podcast prefetch); they’re overkill and complex for normal API calls.
52. Codable performance?
J: JSONDecoder is generally fast; pre-decoded Data to model.
M: Decoding huge arrays of small types can be slow due to repeated keyed container lookups. For megabyte+ payloads, consider streaming or JSONSerialization for selective parsing.
S: I profile decode time in Instruments for any payload > 100kB. Wins: avoid wrapping single-property containers (extra allocation per item), use snake_case decoding strategy rather than per-property CodingKeys where possible, and stream paginated APIs rather than loading everything. For binary protocols, BinaryCodable or Protobuf beats JSON by 5–10×.
53. Networking error handling strategy?
J: Catch errors at the call site, show alerts on failure.
M: Define a typed error enum (enum APIError: Error { case network, decoding, server(Int, String) }) and translate URLError, DecodingError, HTTP status codes into it.
S: Errors are part of the API contract. I distinguish recoverable (retry — network blip, 5xx) from non-recoverable (4xx, decode failures = bug, propagate to error tracking). Use exponential backoff for retries; never retry POST without idempotency keys; surface user-actionable messages without leaking technical detail.
54. Authentication: where to store tokens?
J: Keychain — never UserDefaults.
M: Keychain Services API; for cross-device sync, iCloud Keychain via kSecAttrSynchronizable. Wrap with a typed KeychainStore interface.
S: For OAuth refresh tokens, store in Keychain with kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly (won’t sync to other devices, won’t be in iCloud backups). For access tokens that are short-lived, in-memory only. Implement automatic refresh in a request interceptor; serialize refresh attempts to avoid stampede.
55. Pagination strategies?
J: Offset/limit; cursor-based.
M: Cursor is preferred for live data (no skipped items when new data inserts). Implement infinite scroll by triggering load on visible-row threshold.
S: For client UX, optimistic insert (prepend new items immediately, reconcile after server confirm) eliminates perceived latency. For very large datasets, virtual scrolling with windowing — but LazyVStack covers most needs. Trickiest: handling concurrent edits to the same page across devices.
56. CoreData vs SwiftData?
J: SwiftData (iOS 17+) is the modern, Swift-native replacement for Core Data.
M: SwiftData uses the @Model macro; under the hood it’s Core Data with a nicer API. Same migration, sync, and CloudKit story.
S: For new iOS 17+ apps, SwiftData with the @Model macro. For legacy code or iOS 16 support, Core Data. SwiftData is still maturing — expect rough edges around complex predicates, batch operations, and some sync scenarios. The interop story is good: a Core Data model can be exposed as SwiftData incrementally.
57. CloudKit basics?
J: Apple’s iCloud backend; database-as-a-service with private and public scopes.
M: Three databases: private (user’s iCloud), shared (collaboration), public (app-wide). Records are CKRecord with typed fields. Push notifications notify clients of changes.
S: CloudKit shines for personal-data sync (notes, journals, fitness) where Apple’s free tier and zero-config auth are killer features. It fails for: heavy server-side logic (no compute), arbitrary querying, cross-platform (only Apple devices), and apps needing custom auth. For those, build a real backend.
58. Image caching strategy?
J: Library like Kingfisher or Nuke; for simple cases, URLCache.
M: Two-tier cache: in-memory (NSCache) for hot images, disk for cold. Decode on background thread; downsample large images to displayed size.
S: For lists with many images, prefetching the next N images during scroll prevents pop-in. Downsampling at decode time (not display time) saves memory by 5–10×. For Retina displays, downsample to targetSize * UIScreen.main.scale. Avoid setting image on a UIImageView from an async callback if the cell was already reused — pass an identifier and check before setting.
59. WebSockets in iOS?
J: URLSessionWebSocketTask (iOS 13+) provides built-in WebSocket support.
M: webSocketTask(with:) returns a task; .receive() returns the next message as async value.
S: For chat/real-time apps, WebSockets are the standard. Implement: heartbeat ping every 30s, automatic reconnect with backoff, queue outgoing messages during disconnect. For massive scale, prefer a vendor (Pusher, Ably, PubNub) or roll your own atop Vapor or a Go backend. SSE (Server-Sent Events) is simpler when one-way streaming suffices.
60. Combine vs AsyncStream?
J: Combine is reactive streams; AsyncStream is async iteration.
M: Combine has rich operators (debounce, combineLatest); AsyncStream is bare but integrates with async/await natively.
S: New code: AsyncStream/AsyncSequence. For UI debounce, throttle, combine-latest patterns, AsyncAlgorithms (Apple’s Swift Algorithms for async) covers most needs. Combine remains for KVO/Notification bridging; gradually retire it. For background pipelines that need backpressure, AsyncStream with bufferingPolicy: .bufferingNewest(N).
61. Push notification delivery model?
J: APNs delivers via Apple’s servers; app receives in foreground (userNotificationCenter:willPresent:) or background.
M: Silent push (content-available: 1) wakes the app for background work; user-visible push displays UI. Token-based (.p8) or certificate (.p12) auth to APNs.
S: Silent push is throttled by Apple; don’t rely on it for time-critical updates. For high-frequency real-time updates, WebSocket beats silent push. Always store the latest push payload in your backend even if delivery fails — APNs is best-effort, not guaranteed.
62. Background tasks?
J: BGTaskScheduler (iOS 13+) schedules background app refresh and processing tasks.
M: Register tasks at launch; schedule with BGAppRefreshTaskRequest or BGProcessingTaskRequest. System decides when to run.
S: iOS aggressively kills apps; expect background tasks to run less often than you ask for. For reliable delivery use push + server-side state. For periodic sync, schedule but never assume timing. Wake-from-suspend has tight CPU/network budgets — keep tasks short and resumable.
63. Concurrent network requests?
J: Use async let for parallel awaits; TaskGroup for dynamic counts.
M: async let is good for a fixed handful; TaskGroup for collections.
S: For N independent calls, parallelism speeds up wall time but increases peak memory. For very large N, throttle with a semaphore or withThrowingTaskGroup + for try await with a concurrency cap. Apple’s AsyncAlgorithms’ chunked(into:) and mapAsync(maxConcurrency:) help. Always profile — sometimes serial is fast enough and simpler.
64. GraphQL vs REST in iOS?
J: GraphQL fetches exactly the fields you ask for; REST returns fixed payloads. M: Apollo iOS is the dominant GraphQL client; generates type-safe queries. S: GraphQL shines for clients that need many variants of the same data (different feeds, different screens). The cost is server complexity and tooling overhead. For most B2C apps, well-designed REST with good caching is simpler. I’ve seen GraphQL adopted for frontend velocity reasons (no backend changes needed for new fields) and abandoned for backend complexity reasons (caching, N+1, schema sprawl).
65. Offline-first design?
J: Persist all data locally; sync to server in background. M: Local-first store (SwiftData/Realm/SQLite); conflict resolution policy (last-write-wins, three-way merge, CRDT). S: True offline-first requires designing the data model with conflict resolution in mind from day one. CRDTs (e.g., Yjs in iOS via Yniffer or custom Swift CRDTs) provide automatic merge. Simpler: per-field last-write-wins with timestamps + manual conflict resolution UI for irreconcilable cases. Watch out for clock skew — use server timestamps or vector clocks for ordering.
Architecture (15)
66. When MVC, MVVM, VIPER, or TCA?
See 12.1–12.5. Quick answer: MVC for small; MVVM for testable single screens; Clean/VIPER for domain-heavy enterprise; TCA for unidirectional + exhaustive testing.
67. Coordinator pattern?
J: Pulls navigation logic out of view controllers into a Coordinator object.
M: Parent coordinator owns children; each coordinator handles one flow.
S: Coordinators decouple navigation from screen logic, easing deep linking and tests. With NavigationStack (iOS 16+), much of this is state-based in SwiftUI; UIKit codebases still benefit. The hard part is communication back to coordinator on completion — closures, delegate, or Combine.
68. Single source of truth?
J: Each piece of state lives in one canonical location; other places derive from it.
M: SwiftUI’s @State/@Observable enforce this; in UIKit, you must discipline yourself.
S: Violations create the “stale UI” class of bugs (two views showing different values for the same logical thing). For server-synced state, the server is the truth and local store is a cache. For local-only state, pick the layer (model, view model) and never duplicate.
69. Repository pattern?
J: Abstracts data access behind a protocol; underlying source (network, cache, DB) can change.
M: protocol UserRepository { func fetch(id: UUID) async throws -> User }. Implementations: RemoteUserRepo, CachedUserRepo, MockUserRepo.
S: Repositories are testability sweet spot — swap mock for real. The cache layer can wrap remote: CachedUserRepo(remote: RemoteUserRepo(...), store: SwiftDataStore()). Avoid leaking implementation types into the protocol (e.g., don’t return NSManagedObject).
70. State management beyond SwiftUI?
J: For shared mutable state, central store (TCA, Redux-style) or actor.
M: Avoid singletons; pass dependencies. For cross-screen state, use composition root.
S: The trap: every app eventually grows a “current user” singleton, then a “current document,” then a “current network state.” The solution is one composition root (an AppDependencies-style container) injected via DI, with state owned by typed stores or actors. SwiftUI’s environment is one mechanism; constructor injection is the other.
71. Feature flags?
J: Toggle features on/off without redeploying via a remote config. M: Vendors: Firebase Remote Config, LaunchDarkly, Statsig. Wrap behind a typed interface. S: Feature flags enable trunk-based development and gradual rollouts. Discipline: every flag has an owner and an end-of-life date. Otherwise codebases accumulate decade-old flags. For experimentation, flags with cohort assignment + analytics integration.
72. A/B testing infrastructure?
J: Assign users to variants; track outcomes; compare statistically.
M: Vendors handle assignment (sticky per user) + analytics. Critical: don’t change a user’s variant mid-experiment.
S: I integrate experiments behind a typed Experiment<Variant> interface so the rest of the code doesn’t know about the testing infrastructure. For client-side experiments, the variant assignment should be deterministic given the user ID (no network call needed for assignment). Server-side flags are simpler but tie variant changes to deploys.
73. Push-to-test architecture?
J: Ability to push a change to a test cohort first. M: Combine feature flags + TestFlight + analytics segmentation. S: Mature shops have a “canary” cohort (internal users + opted-in beta) receiving every change first, then a 1 %, 10 %, 50 %, 100 % gradual rollout. The infra cost is real but the safety net for a 10M+ user app is invaluable.
74. Mobile + backend contract design?
J: REST API or GraphQL schema; mobile and backend agree on shape.
M: Backward compatibility: never break existing client versions. Mobile clients can’t be force-updated overnight.
S: API versioning is a discipline: every change is additive (add fields, never remove or rename). Old fields are deprecated, kept indefinitely. Minimum supported app version forces eventual cleanup. For breaking changes, use new endpoint versions (/v2/) with mobile detecting and routing. Schema-first design tools (OpenAPI, GraphQL SDL) catch issues early.
75. Local + remote state reconciliation?
J: Optimistic update locally; if server rejects, revert. M: Pattern: send mutation, mark local item as “pending”; on success, mark “synced”; on failure, mark “failed” with retry. S: For multi-step user flows, accumulate optimistic mutations in a queue; replay in order with reconciliation. CRDTs make this automatic; explicit code requires careful handling of out-of-order responses and partial failures. Show “syncing” UI for transparency; don’t hide partial-state failures.
76. SwiftUI state pyramid?
J: Local view state in @State; shared state lifted to nearest common ancestor.
M: Don’t lift state higher than necessary — keeps re-render scope small.
S: The hardest call is “do I lift this to app scope or keep it per-screen?” Rule of thumb: if two screens need the same state, lift to their nearest common ancestor (often the navigation parent). For global concerns (current user, theme), lift to root via @Environment. Premature global state makes testing hard.
77. Reactive vs imperative?
J: Reactive describes state→UI as a function; imperative mutates UI step by step. M: SwiftUI is reactive; UIKit is imperative (with reactive extensions via Combine). S: Reactive scales better to complex UI but has a steeper learning curve (identity, animations, debugging are harder). Imperative gives finer control but more bugs around state synchronization. Most modern iOS work is reactive; legacy is imperative. Mixing per screen is fine; mixing within a screen is asking for bugs.
78. Modularization heuristic?
See 12.6. TL;DR: extract when builds slow or merge conflicts spike; otherwise wait.
79. Server-driven UI?
J: Server sends layout descriptions (JSON), client renders dynamically. M: Used by Airbnb (Epoxy + DLS), Netflix (Falcor + UI specs). Reduces app updates for content changes. S: Powerful for content-heavy apps where layouts change often; overkill for traditional product UI. The cost: client must support a UI DSL (a mini interpreter), and the server team owns layout decisions. For apps with weekly App Store reviews wanted as a release valve, it’s a strategic investment.
80. Plugin/extension architecture?
J: App can load additional functionality dynamically. M: iOS app extensions (share, widgets, intents) are sandboxed bundles. Internal modularization via SPM features is a softer form. S: For consumer apps, plugin systems are rare (App Store review wants known surfaces). For developer tools (Xcode, BBEdit), they’re essential. Internal “plugin” patterns — feature flagged modules, dynamic config — give similar flexibility without the security/review headache.
Performance (10)
81. Profiling tools?
J: Instruments — Time Profiler, Allocations, Leaks, Network, Energy. M: Time Profiler shows what’s running on each thread; Allocations tracks heap growth; SwiftUI template for view body churn. S: I profile before optimizing. The intuition for “this is slow” is usually wrong. Time Profiler with “Hide System Libraries” and “Invert Call Tree” surfaces user code hotspots. For energy, the Energy Log instrument on device is the only reliable measure — Simulator is misleading.
82. Main thread blocking — finding it?
J: App freezes; Main Thread Checker reports during debug.
M: Instruments’ Time Profiler with main thread filter. Watch for “Hangs” warnings in Xcode Organizer for production crashes.
S: Common culprits: large JSON decoding, image decoding, synchronous file I/O, Core Data fetches without performBackgroundTask. The fix is moving to background queue or Task.detached; the discipline is having a code review rule that any data(contentsOf:) outside Task fails review.
83. App launch optimization?
J: Reduce work done in application(_:didFinishLaunchingWithOptions:).
M: Instruments’ “App Launch” template breaks down dyld, ObjC runtime, app init. Defer non-essential setup.
S: For complex apps, “first frame” is the metric — measured via os_signpost from applicationDidFinishLaunching to first UIWindow.makeKeyAndVisible. Wins: lazy-init analytics, push registration after first view, async-init persistence stores. iOS 16+ MetricKit + Xcode Organizer give per-version launch percentiles in production.
84. Memory leaks vs retain cycles?
J: Leak = unreachable memory not deallocated; retain cycle = mutual strong references. M: Instruments’ Leaks instrument finds true leaks; cycles often need Memory Graph Debugger (Xcode → Debug Memory Graph). S: Most “leaks” in iOS are retain cycles, not true leaks. Memory Graph Debugger shows the strong-reference graph — find cycles visually. Common: closure captures, delegate not weak, NotificationCenter observers not removed (less common with block-based API). For NSCache vs Dictionary: NSCache auto-evicts under pressure.
85. Image performance?
J: Downsample images at load time, not display time.
M: CGImageSourceCreateThumbnailAtIndex with kCGImageSourceThumbnailMaxPixelSize. Decode off main thread.
S: Big wins in feeds: progressive JPEG for perceived speed, downsample to displayed size (counted in pixels not points), recycle UIImage allocations via cache, prefetch via UICollectionView/UITableView prefetch APIs. For HEIC, decode is faster but encoding takes longer — fine for assets.
86. Scroll performance?
J: Aim for 60fps; profile in Instruments → Core Animation.
M: Avoid offscreen rendering (corner radius + masksToBounds), reduce layer count, cache cell heights.
S: For 120Hz ProMotion devices, target 120fps. Common wins: pre-compose static layers once (shouldRasterize), use opaque backgrounds (isOpaque = true), avoid shadows or use shadowPath. For text-heavy cells, profile attributed string rendering — NSAttributedString parsing can dominate.
87. Battery drain?
J: Background work, GPS, network all drain battery.
M: Energy Log instrument shows draw per subsystem.
S: Common offenders: location with kCLLocationAccuracyBest always-on, background uploads with cellular fallback, frequent CPU wakeups, animations that keep the display on. Use CLLocationManager.allowsBackgroundLocationUpdates only when needed; coalesce network into batches; respect Low Power Mode (ProcessInfo.processInfo.isLowPowerModeEnabled).
88. Networking efficiency?
J: Compress requests/responses (gzip), use HTTP/2, cache aggressively.
M: URLSessionConfiguration.allowsConstrainedNetworkAccess, allowsExpensiveNetworkAccess for Low Data Mode. Use HTTP/3 (QUIC) where server supports.
S: For chatty APIs, batch endpoints save round trips. Cache headers (Cache-Control, ETag) reduce redundant transfers. Image CDN with content negotiation (WebP/AVIF for supported clients). For bandwidth-constrained users, downsample images on the server based on Save-Data header.
89. Core Data performance?
J: Fetch in batches; use NSFetchedResultsController for table-driven UI.
M: Background contexts for writes; merge to view context for reads. setReturnsObjectsAsFaults(false) to materialize objects.
S: Common pitfalls: faulting in loops (N+1 fault queries), holding objects across context boundaries, unindexed predicates causing full-table scans. Profile with SQL Debug (-com.apple.CoreData.SQLDebug 1). For very large stores, denormalize for read-heavy queries.
90. SwiftUI render performance?
J: Use _printChanges() and Instruments’ SwiftUI template.
M: Lazy stacks, equatable views, minimal @Observable property reads in body.
S: For lists of 1000+ items, ensure LazyVStack is used (not VStack) and each row is Equatable. Hoist filtering/sorting out of body — compute once, store in @State. Avoid GeometryReader in cells. The @Observable macro’s per-property tracking is a major win over ObservableObject.
Security (10)
See Phase 9 — Security for deep coverage. Quick answers:
91. Keychain best practices?
Store credentials with kSecAttrAccessibleWhenUnlockedThisDeviceOnly by default; never use UserDefaults; wrap with typed Swift API.
92. ATS (App Transport Security)?
Require HTTPS by default; exceptions only with technical justification (legacy backend on the migration path).
93. Jailbreak detection?
Best-effort, never security; check for known paths (/Applications/Cydia.app), URL(string: "cydia://"), fork() succeeding. Defense in depth: sensitive ops should validate server-side too.
94. Code obfuscation in Swift?
Limited utility — Swift compiles to native code already. Strip symbols in Release. For high-value IP, server-side execution beats client obfuscation.
95. Certificate pinning?
Pin to a SubjectPublicKeyInfo hash to survive cert rotation; never pin to a leaf cert that rotates frequently; ship multiple pins (current + next-rotation backup) to prevent app-bricking.
96. Secure storage of API keys?
Don’t ship API keys in plist or code — they’re trivially extractable. Use server-mediated auth (mobile → your backend → third-party API) or short-lived tokens issued by your backend.
97. OWASP Mobile Top 10 highlights?
Improper credential storage, insufficient cryptography, insecure communication, code tampering. Map each to iOS controls: Keychain, CryptoKit, ATS, TestFlight + server-side validation.
98. WebView XSS?
Treat WKWebView like a browser: never inject untrusted HTML, use WKContentRuleList to block scripts, validate evaluateJavaScript inputs.
99. Deep link validation?
Treat URL parameters as untrusted input. Validate schemes (only your app’s), sanitize parameters before use, never auto-execute actions without user confirmation.
100. Privacy: what data goes where?
Privacy Manifest (iOS 17+) declares data types collected. App Tracking Transparency (ATTrackingManager) gates IDFA. Audit third-party SDKs — they’re often the data leakage.
How to use this list
- Print it. Skim weekly.
- Pick five questions a week, write your answers, then read the senior level.
- Note the gaps. The senior answers reveal what experience teaches.
- For your target role, ensure you can answer your level and one level up.
Phase 12.8 explains the three-level system itself — once you internalize it, you’ll spot it in every interview answer for the rest of your career.