Back to interview questions

100+ iOS Interview Questions and Answers

100+ iOS Interview Questions and Answers

Explore 120 iOS interview questions and answers on Swift, UIKit, SwiftUI, concurrency, networking, persistence, testing, security, and app architecture.

Explore 120 iOS interview questions and answers on Swift, UIKit, SwiftUI, concurrency, networking, persistence, testing, security, and app architecture.

By

Team Sherlock.sh

Add Sherlock as a preferred source on Google

iOS interviews test whether candidates can turn Swift code into reliable experiences on Apple devices. Strong developers understand app and scene lifecycles, UI state, memory ownership, concurrency, and the constraints of mobile networking and background work. Recruiters should look for explanations that connect implementation choices to accessibility, responsiveness, privacy, testing, and maintainable releases.

This guide to 100+ iOS Interview Questions and Answers contains 120 questions across beginner, intermediate, and experienced levels. It covers Swift, UIKit, SwiftUI, structured concurrency, networking, persistence, security, performance, architecture, and delivery. The questions distinguish language features from operating-system APIs so an interview can assess real engineering judgment rather than memorization of one SDK version.

How can recruiters detect potential AI-assisted cheating, deepfakes, impersonation, and unauthorized assistance during iOS interviews?

An iOS candidate may be asked to explain a retain cycle, diagnose a stale SwiftUI screen, or repair an actor-isolation error. A polished answer does not necessarily demonstrate independent understanding of Swift ownership, UI state, or asynchronous execution when unauthorized real-time assistance is involved.

Sherlock AI helps recruiters detect potential AI-assisted cheating, deepfakes, candidate impersonation, and unauthorized real-time assistance during iOS interviews.

Pair interview-integrity checks with a focused debugging task. Change a model lifetime, delay a network response, or ask the candidate to explain a deployment-target constraint and test the resulting behavior. Review suspicious signals in context rather than treating any single signal as proof of misconduct.

iOS Interview Questions

Choose questions that match the role and application. Specify the supported iOS versions, Xcode and Swift toolchain, concurrency settings, and UIKit or SwiftUI context before judging a solution. A junior UI role and a senior engineer responsible for offline data, performance, and releases require different depth.

iOS Interview Questions for Beginners

Start with Swift types, optional handling, ownership, app and scene lifecycles, UIKit fundamentals, and SwiftUI state. These questions establish whether a candidate can explain a small application and recognize basic networking, storage, accessibility, and testing responsibilities.

  1. What is iOS, and how is it different from Swift?

  2. What are the roles of Xcode, the iOS SDK, and the deployment target?

  3. How do you use availability checks for newer iOS APIs?

  4. What is the difference between let and var in Swift?

  5. What is an Optional, and why does Swift use it?

  6. How do if let, guard let, and nil coalescing differ?

  7. When are forced unwrapping and implicitly unwrapped optionals risky?

  8. How do structures and classes differ in Swift?

  9. What are enums and associated values useful for?

  10. How do Array, Set, and Dictionary differ?

  11. What is a closure, and what does capturing a value mean?

  12. What is the difference between escaping and nonescaping closures?

  13. What is a protocol, and how is it different from inheritance?

  14. What are extensions and generics used for?

  15. How does Swift handle recoverable errors?

  16. What is Automatic Reference Counting?

  17. How do weak and unowned references differ?

  18. How can a closure create a retain cycle?

  19. What is the difference between an app lifecycle and a scene lifecycle?

  20. What should an iOS app do when it enters the background?

  21. What is a UIViewController responsible for?

  22. How do viewDidLoad, viewWillAppear, and layout callbacks differ?

  23. Why should UI work be isolated to the main actor?

  24. What problem does Auto Layout solve?

  25. What are delegates and data sources in UIKit?

  26. Why do table and collection views reuse cells?

  27. How do push navigation and modal presentation differ in UIKit?

  28. Why use asset catalogs and semantic colors?

  29. How does SwiftUI differ from UIKit?

  30. What does the body property of a SwiftUI view represent?

  31. What is State used for in SwiftUI?

  32. How is Binding different from State?

  33. How do Observable models, StateObject, and ObservedObject fit together?

  34. What are NavigationStack and NavigationLink used for in SwiftUI?

  35. What is URLSession used for?

  36. What are Codable and JSONDecoder?

  37. When should you use UserDefaults, files, or a database?

  38. Why should credentials go in the Keychain rather than UserDefaults?

  39. What makes a basic iOS interface accessible?

  40. What is the difference between unit tests and UI tests in iOS?

iOS Interview Questions for Intermediate

Explore reliable implementation with UIKit and SwiftUI, structured concurrency, HTTP networking, persistence, dependency injection, and tests. Ask how the proposed design behaves during cancellation, navigation, backgrounding, permission denial, or reuse.

  1. How do generics, opaque types, and existential types differ in Swift?

  2. Why can a protocol extension method behave differently from a protocol requirement?

  3. How do you choose a closure capture strategy without creating a retain cycle?

  4. How would you investigate a view controller that does not deallocate?

  5. What is the correct way to embed a child view controller?

  6. How do you prevent reused cells from showing an incorrect downloaded image?

  7. How does a diffable data source update a collection view reliably?

  8. What makes a self-sizing table or collection cell work correctly?

  9. How does SwiftUI view identity affect state and task lifetime?

  10. How does Observation change SwiftUI model updates?

  11. How do you design SwiftUI bindings without creating competing sources of truth?

  12. How do you implement programmatic navigation with NavigationStack?

  13. How should UIViewRepresentable coordinate UIKit and SwiftUI state?

  14. How do SwiftUI layout proposals differ from Auto Layout constraints?

  15. What does MainActor guarantee, and what does async not guarantee?

  16. Why can an actor still have a race in its business logic?

  17. When should you use Task, Task.detached, or a structured child task?

  18. When is async let preferable to sequential awaits?

  19. How do task groups handle a dynamic number of concurrent operations?

  20. How do you implement cooperative cancellation correctly?

  21. What does Sendable mean, and when is unchecked conformance dangerous?

  22. What are the rules for wrapping a callback API with a continuation?

  23. When would you use AsyncSequence or AsyncStream?

  24. How do you validate a URLSession response before decoding it?

  25. How do you make Codable decoding resilient without hiding schema errors?

  26. How do HTTP caching and an in-memory model cache differ?

  27. When is retrying a failed network request safe?

  28. How do you use Core Data safely from background work?

  29. How should you plan a SwiftData schema migration?

  30. How does dependency injection make an iOS feature easier to test?

  31. When should you use Swift Testing instead of XCTest?

  32. How do you test asynchronous behavior without flaky sleeps?

  33. How do you support Dynamic Type without breaking the layout?

  34. How do you make a custom control useful with VoiceOver?

  35. How should an iOS app store and retrieve a secret in the Keychain?

  36. How do you request privacy-sensitive permissions responsibly?

  37. How do you choose between a background task and a background URLSession?

  38. How do notification delivery and notification handling differ?

  39. How do you validate and route a universal link?

  40. How do you manage Combine subscriptions and UI updates safely?

iOS Interview Questions for Experienced

Use these scenarios to assess architecture and ownership of production outcomes. Strong responses connect product requirements to lifecycle boundaries, consistency, security, measurable performance, accessibility, migration, and release controls.

  1. How would you choose an architecture for a growing iOS application?

  2. How would you modularize an iOS codebase without creating excessive build complexity?

  3. How would you design navigation that supports deep links and multiple presentation styles?

  4. How would you separate shared application state from per-scene state?

  5. How would you make state restoration resilient to application and data changes?

  6. How would you migrate a UIKit application to SwiftUI incrementally?

  7. How would you migrate a large codebase to Swift 6 concurrency checking?

  8. How would you decide where CPU-intensive work should execute?

  9. How would you protect business invariants across actor suspension points?

  10. How would you coordinate authentication refresh when many requests expire together?

  11. How would you design offline editing and conflict resolution?

  12. How would you prevent duplicate server-side operations after retries or app restarts?

  13. How would you make large file transfers survive suspension?

  14. How would you choose an iOS background execution strategy?

  15. How would you design push notification handling so it remains correct when delivery is unreliable?

  16. How would you secure deep links and diagnose universal-link failures?

  17. How would you design secure credential storage and logout behavior?

  18. How would you evaluate ATS exceptions and certificate pinning?

  19. How would you keep privacy declarations aligned with application behavior?

  20. How would you assess a third-party iOS SDK before adopting or upgrading it?

  21. How would you plan a persistent-store migration without risking user data?

  22. How would you investigate a regression in application launch time?

  23. How would you diagnose an intermittent UI hang?

  24. How would you improve scrolling performance in a media-heavy feed?

  25. How would you distinguish a memory leak from excessive but temporary memory use?

  26. How would you reduce battery usage without compromising required behavior?

  27. How would you investigate a crash reported only in production?

  28. How would you design privacy-conscious production observability for an iOS app?

  29. How would you choose the right balance of unit, integration, and UI tests?

  30. How would you make asynchronous tests deterministic?

  31. How would you make accessibility part of release quality rather than a final checklist?

  32. How would you prepare an iOS application for localization and regional differences?

  33. How would you design a reliable iOS CI and signing pipeline?

  34. How would you use TestFlight and phased release to reduce deployment risk?

  35. How would you respond to a severe defect discovered after an App Store release?

  36. How would you choose a deployment target and adopt newer platform APIs safely?

  37. How would you maintain a safe boundary between Swift and Objective-C code?

  38. How would you share data safely between an app and its extensions?

  39. How would you design caches that remain correct across accounts and changing data?

  40. How would you lead a technical review of an unfamiliar iOS application?

iOS Interview Questions and Answers

These answers follow the question bank in the same order and begin with direct responses candidates can adapt in an interview. Swift examples illustrate the named concept; surrounding application configuration and unrelated production infrastructure are intentionally omitted. Verify SDK availability, deployment targets, and isolation settings before adapting an example.

iOS Interview Questions and Answers for Beginners

The beginner answers connect Swift language concepts to common iOS application behavior. They emphasize safe value handling, explicit ownership, appropriate UI state, and the difference between a language feature and an operating-system API.

1. What is iOS, and how is it different from Swift?

iOS is Apple's operating system for iPhone; Swift is a programming language used to build applications for iOS and other platforms. An iOS application combines language code with platform frameworks such as UIKit, SwiftUI, and Foundation. Knowing Swift syntax is therefore necessary for many iOS roles, but it does not by itself demonstrate knowledge of application lifecycle, permissions, interface conventions, or device constraints.

For example, a Swift structure that calculates an invoice total can be platform-independent. A screen that requests camera access and displays a preview depends on iOS frameworks and permission behavior. The language compiler and the operating system also have separate versions: a compiler may support a language feature while an API used by the code still requires a newer deployment target.

I separate these concerns when explaining a solution. I identify the language behavior first, then the framework API, supported iOS versions, and assumptions about the device or simulator.

2. What are the roles of Xcode, the iOS SDK, and the deployment target?

Xcode is the development environment, the iOS SDK supplies build-time platform interfaces and tools, and the deployment target declares the oldest iOS version the application supports. These settings are related but not interchangeable. Building with a newer SDK does not mean every API in that SDK is available on every supported device.

A developer can compile an application with a recent SDK while supporting older iOS releases, provided the code avoids unavailable APIs or protects them with availability checks. The Swift language mode is another setting; changing it can enable stricter language checks without changing the minimum operating-system version automatically.

In an interview, I would inspect the project settings before choosing an API. I would also distinguish a simulator build from a device build and verify required capabilities, entitlements, and signing separately. A successful local build proves neither that the app has the necessary runtime permission nor that the same workflow works on physical hardware.

3. How do you use availability checks for newer iOS APIs?

Use if #available when selecting a runtime code path and @available when declaring that an API or type requires a particular operating-system version. The compiler uses these annotations to prevent unguarded calls from code that can execute on older systems.

Swift example

import SwiftUI

struct SettingsRoot: View {
    var body: some View {
        if #available(iOS 16.0, *) {
            NavigationStack { Text("Settings") }
        } else {
            NavigationView { Text("Settings") }
        }
    }
}

import SwiftUI

struct SettingsRoot: View {
    var body: some View {
        if #available(iOS 16.0, *) {
            NavigationStack { Text("Settings") }
        } else {
            NavigationView { Text("Settings") }
        }
    }
}

import SwiftUI

struct SettingsRoot: View {
    var body: some View {
        if #available(iOS 16.0, *) {
            NavigationStack { Text("Settings") }
        } else {
            NavigationView { Text("Settings") }
        }
    }
}

Here the older navigation type is a compatibility fallback, not the preferred architecture for a new iOS 16-or-later application. A feature may also require hardware, an entitlement, authorization, or an available service; an OS-version check does not establish those conditions. I avoid checking device model strings to infer API support and keep the fallback functional rather than merely hiding a compiler error.

4. What is the difference between let and var in Swift?

let declares a binding that cannot be reassigned after initialization; var declares a mutable binding. For a value type, a constant binding also prevents mutation of its stored value through that binding. For a class instance, let prevents replacing the reference but does not automatically freeze the object's mutable properties.

Swift example

struct CartItem {
    var quantity: Int
}
let fixedItem = CartItem(quantity: 1)
var editableItem = fixedItem
editableItem.quantity = 2
// fixedItem.quantity remains 1.

struct CartItem {
    var quantity: Int
}
let fixedItem = CartItem(quantity: 1)
var editableItem = fixedItem
editableItem.quantity = 2
// fixedItem.quantity remains 1.

struct CartItem {
    var quantity: Int
}
let fixedItem = CartItem(quantity: 1)
var editableItem = fixedItem
editableItem.quantity = 2
// fixedItem.quantity remains 1.

I use constants by default because they narrow the possible changes a reader must consider. When a value needs to evolve, I make the mutation explicit and keep its scope small. A constant reference is not a concurrency strategy: if several tasks can mutate the same object, the object still needs appropriate isolation. Similarly, changing a local variable does not automatically notify a UI framework that its display should update.

5. What is an Optional, and why does Swift use it?

An Optional represents either a wrapped value or the absence of a value. String?, for example, can contain a string or nil; an ordinary String cannot represent missing data using nil. This makes missing-value handling part of the type contract rather than an undocumented convention.

Swift example

let text = "42"
let count: Int? = Int(text)
if let count {
    print(count + 1)
}

let text = "42"
let count: Int? = Int(text)
if let count {
    print(count + 1)
}

let text = "42"
let count: Int? = Int(text)
if let count {
    print(count + 1)
}

The integer initializer can fail, so the result is optional. Optional binding makes the successful value available without a forced unwrap. I distinguish absence from a valid empty value: an empty name and a missing name may mean different things to a business rule.

I also avoid making every field optional merely to silence decoding or initialization errors. A required server identifier should usually remain required, so malformed input is rejected instead of creating an apparently valid but unusable model.

6. How do if let, guard let, and nil coalescing differ?

if let unwraps an optional inside a conditional scope, guard let exits the current scope when unwrapping fails, and ?? supplies a fallback value. The choice communicates what missing data means for the surrounding operation.

Swift example

func greeting(for name: String?) -> String {
    guard let name, !name.isEmpty else {
        return "Welcome"
    }
    return "Welcome, \(name)"
}
let caption = Optional<String>.none ?? "Untitled"

func greeting(for name: String?) -> String {
    guard let name, !name.isEmpty else {
        return "Welcome"
    }
    return "Welcome, \(name)"
}
let caption = Optional<String>.none ?? "Untitled"

func greeting(for name: String?) -> String {
    guard let name, !name.isEmpty else {
        return "Welcome"
    }
    return "Welcome, \(name)"
}
let caption = Optional<String>.none ?? "Untitled"

I use guard for prerequisites because the successful path stays at the main indentation level. if let is convenient when only one small branch needs the value. Nil coalescing is appropriate when the fallback has genuine meaning; substituting zero for a missing invoice amount could conceal invalid data.

A guard's failure branch must leave the scope, such as by returning or throwing. None of these constructs makes an asynchronous result available sooner; they handle a value that already exists at that point.

7. When are forced unwrapping and implicitly unwrapped optionals risky?

Forced unwrapping with ! asserts that an optional currently contains a value. If that assertion is false, execution traps. An implicitly unwrapped optional is still optional storage, but the compiler can unwrap it automatically in contexts that require its wrapped type. Neither construct is a substitute for validation.

A connected UIKit outlet is a common constrained use: it may be nil before the view loads and expected to exist afterward. Accessing it too early, removing its storyboard connection, or using a different loading path can violate that expectation. I prefer ordinary optionals for data that can legitimately be missing and nonoptional initialization when a value is truly required.

During code review, I ask what invariant proves the unwrap safe and whether that invariant can change with user input, network data, or lifecycle timing. If failure is recoverable, optional binding or a throwing function expresses the behavior more honestly than a potential crash.

8. How do structures and classes differ in Swift?

Structures have value semantics, while classes have reference semantics and object identity. Assigning a structure produces an independently usable value; assigning a class reference makes another reference to the same instance. Both can define properties, initializers, methods, and protocol conformances, but classes also support inheritance.

Swift example

struct Score { var points: Int }
var first = Score(points: 10)
var second = first
second.points = 20
// first.points is still 10.

struct Score { var points: Int }
var first = Score(points: 10)
var second = first
second.points = 20
// first.points is still 10.

struct Score { var points: Int }
var first = Score(points: 10)
var second = first
second.points = 20
// first.points is still 10.

I generally choose a structure for domain data such as a coordinate or a decoded record. A class is useful when shared identity or framework inheritance is required, as with a UIKit view controller. Value semantics are not a claim that every assignment eagerly copies every byte; Swift collections can use copy-on-write optimizations.

A structure containing a mutable class reference can still expose shared mutable state through that reference. I therefore inspect its stored properties before describing a custom type as deeply independent.

9. What are enums and associated values useful for?

An enum models a finite set of alternatives, and associated values attach data that belongs to a particular alternative. This is useful when several Boolean flags would allow contradictory states. A network result, for example, should not be both loading and failed at the same time.

Swift example

enum DownloadState {
    case idle
    case loading
    case complete(byteCount: Int)
    case failed(message: String)
}
func summary(_ state: DownloadState) -> String {
    switch state {
    case .idle: return "Not started"
    case .loading: return "Loading"
    case .complete(let bytes): return "Received \(bytes) bytes"
    case .failed(let message): return message
    }
}

enum DownloadState {
    case idle
    case loading
    case complete(byteCount: Int)
    case failed(message: String)
}
func summary(_ state: DownloadState) -> String {
    switch state {
    case .idle: return "Not started"
    case .loading: return "Loading"
    case .complete(let bytes): return "Received \(bytes) bytes"
    case .failed(let message): return message
    }
}

enum DownloadState {
    case idle
    case loading
    case complete(byteCount: Int)
    case failed(message: String)
}
func summary(_ state: DownloadState) -> String {
    switch state {
    case .idle: return "Not started"
    case .loading: return "Loading"
    case .complete(let bytes): return "Received \(bytes) bytes"
    case .failed(let message): return message
    }
}

Associated values differ from raw values: a raw value is a fixed representation associated with an enum case, while associated data can vary for each instance. Exhaustive switches make newly added cases visible to the compiler. I still consider whether exposing an internal error message directly to users is appropriate before using this example in a real interface.

10. How do Array, Set, and Dictionary differ?

An Array stores an ordered sequence and allows duplicates, a Set stores unique hashable elements without a meaningful presentation order, and a Dictionary maps unique hashable keys to values. I choose the collection according to the operation and data contract rather than treating them as interchangeable containers.

For example, search results belong in an array because ranking matters. A set is useful for tracking selected identifiers or testing membership. A dictionary is useful for looking up a record by identifier. Converting an array to a set removes duplicates but also abandons its ordering, so it is not a harmless optimization for a displayed list.

Dictionary lookup returns an optional because the requested key may be absent. All three standard collections have value semantics. If their elements are class instances, however, copies of the collection can still refer to the same objects. Deterministic UI ordering should be explicit, such as a sorted array, rather than inferred from dictionary iteration.

11. What is a closure, and what does capturing a value mean?

A closure is a callable block of code that can capture values from the context in which it is created. Closures let an API accept behavior, such as a sorting rule or a response to a button press, without requiring a separately named function for every operation.

Swift example

let minimum = 10
let quantities = [3, 12, 18]
let accepted = quantities.filter { quantity in
    quantity >= minimum
}
// accepted is [12, 18].

let minimum = 10
let quantities = [3, 12, 18]
let accepted = quantities.filter { quantity in
    quantity >= minimum
}
// accepted is [12, 18].

let minimum = 10
let quantities = [3, 12, 18]
let accepted = quantities.filter { quantity in
    quantity >= minimum
}
// accepted is [12, 18].

This closure uses minimum from its surrounding context. Capturing an object may keep that object alive when the closure is retained. A capture list can make ownership choices explicit, but adding [weak self] to every closure is not a universal rule.

I explain a closure's parameter and return types before shortening it to $0 syntax. Concise code is useful when the operation remains obvious; named parameters are usually better when several captured values or business conditions are involved.

12. What is the difference between escaping and nonescaping closures?

A nonescaping closure parameter is used before the receiving function returns. An escaping closure may be stored or invoked after that function returns and is declared with @escaping. This distinction describes the closure's possible lifetime, not whether it runs on a background thread.

Swift example

final class Confirmation {
    private var action: (() -> Void)?

    func register(_ action: @escaping () -> Void) {
        self.action = action
    }

    func confirm() {
        let pending = action
        action = nil
        pending?()
    }
}

final class Confirmation {
    private var action: (() -> Void)?

    func register(_ action: @escaping () -> Void) {
        self.action = action
    }

    func confirm() {
        let pending = action
        action = nil
        pending?()
    }
}

final class Confirmation {
    private var action: (() -> Void)?

    func register(_ action: @escaping () -> Void) {
        self.action = action
    }

    func confirm() {
        let pending = action
        action = nil
        pending?()
    }
}

The example retains an action for later and clears it before invoking it, making the action one-shot and reducing unintended retention. Production code would also define which actor owns this object if multiple tasks can access it.

I check escaping closures for ownership cycles and cancellation behavior. An asynchronous API often uses an escaping completion handler, but escaping does not itself guarantee asynchronous execution; an implementation can also invoke the closure immediately.

13. What is a protocol, and how is it different from inheritance?

A protocol defines capabilities a conforming type must provide. Inheritance builds a class hierarchy in which subclasses reuse or specialize superclass behavior. Structures, enums, and classes can all conform to protocols, so protocols are useful for expressing contracts without requiring a shared class ancestor.

Swift example

protocol PriceFormatting {
    func label(for cents: Int) -> String
}
struct PlainPriceFormatter: PriceFormatting {
    func label(for cents: Int) -> String {
        "\(cents) cents"
    }
}

protocol PriceFormatting {
    func label(for cents: Int) -> String
}
struct PlainPriceFormatter: PriceFormatting {
    func label(for cents: Int) -> String {
        "\(cents) cents"
    }
}

protocol PriceFormatting {
    func label(for cents: Int) -> String
}
struct PlainPriceFormatter: PriceFormatting {
    func label(for cents: Int) -> String {
        "\(cents) cents"
    }
}

A checkout presenter can depend on the formatting contract instead of a particular concrete formatter. A test can then supply a predictable implementation. Protocol extensions can provide shared behavior, but the protocol's requirements should still communicate the essential contract.

I do not create a protocol for every type automatically. A useful abstraction has a real boundary, such as multiple implementations or a dependency that should be replaceable in tests. Overly broad protocols can couple unrelated features just as tightly as an oversized base class.

14. What are extensions and generics used for?

Extensions add functionality to an existing type without changing its original declaration, while generics express algorithms or types that work with multiple concrete types under explicit constraints. They solve different problems and are often used together.

Swift example

extension Array where Element: Equatable {
    func containsAll(_ required: [Element]) -> Bool {
        required.allSatisfy { self.contains($0) }
    }
}
let hasRequired = [1, 2, 3].containsAll([1, 3])

extension Array where Element: Equatable {
    func containsAll(_ required: [Element]) -> Bool {
        required.allSatisfy { self.contains($0) }
    }
}
let hasRequired = [1, 2, 3].containsAll([1, 3])

extension Array where Element: Equatable {
    func containsAll(_ required: [Element]) -> Bool {
        required.allSatisfy { self.contains($0) }
    }
}
let hasRequired = [1, 2, 3].containsAll([1, 3])

The extension is available only when array elements support equality. The example checks membership, not duplicate counts: [1] satisfies [1, 1] under this deliberately limited contract. For large collections, repeated linear searches might justify a set-based alternative with a Hashable constraint.

Extensions cannot add stored instance properties or override existing behavior. I use generics when preserving relationships between types improves correctness, not simply to make a small function look abstract. Good names and documented semantics still matter more than clever constraints.

15. How does Swift handle recoverable errors?

Swift uses values conforming to Error, throwing functions, and explicit handling or propagation. A throwing operation is called with try; callers can catch the error, pass it upward, or deliberately convert failure into an optional with try?.

Swift example

enum QuantityError: Error { case invalid }
func parseQuantity(_ text: String) throws -> Int {
    guard let value = Int(text), value > 0 else {
        throw QuantityError.invalid
    }
    return value
}

enum QuantityError: Error { case invalid }
func parseQuantity(_ text: String) throws -> Int {
    guard let value = Int(text), value > 0 else {
        throw QuantityError.invalid
    }
    return value
}

enum QuantityError: Error { case invalid }
func parseQuantity(_ text: String) throws -> Int {
    guard let value = Int(text), value > 0 else {
        throw QuantityError.invalid
    }
    return value
}

A form could catch QuantityError.invalid and show a validation message rather than crash. try! asserts that failure cannot occur and traps if that assumption is wrong, so it is inappropriate for ordinary user or network input.

I distinguish a recoverable error from absence: a missing optional value may be expected, whereas an error explains why an operation failed. I also avoid swallowing errors indiscriminately with try?, because that can erase the information needed to decide whether to retry, ask for input, or report a fault.

16. What is Automatic Reference Counting?

Automatic Reference Counting manages the lifetime of class instances by tracking strong ownership. An instance remains alive while strong references keep it reachable through ownership; when its strong ownership ends, its resources can be released. ARC is automatic memory management, but it does not eliminate the need to design ownership relationships.

A screen might own a presenter, which owns a service. If the service also strongly retains a closure that captures the presenter, that ownership cycle can keep objects alive after the screen disappears. ARC does not trace the entire object graph to collect arbitrary cycles.

I use value types where shared identity is unnecessary, make long-lived ownership explicit, and inspect object lifetimes when memory grows unexpectedly. The deinit method can help observe or clean up a class instance's final release, but it is not a reliable place to save user data that must survive process termination. Object release and application lifecycle are separate concerns.

17. How do weak and unowned references differ?

Neither a weak nor an unowned reference keeps an object alive. A weak reference becomes nil when its referenced object is deallocated, so it is normally an optional variable. An unowned reference expresses a stronger lifetime assumption: the referenced object must still exist whenever the reference is used. Accessing a dangling ordinary unowned reference traps.

I choose weak ownership when the other object may disappear independently, such as a screen serving as a delegate. I consider unowned only when the program's lifetime rules genuinely guarantee that the referenced object outlives every use. Saving a small amount of optional-handling syntax is not sufficient justification.

A weak property can become nil between unrelated operations, so a local strong binding is useful when an operation requires temporary ownership. For closures, [weak self] makes sense when the closure should not extend the owner's lifetime; it can also cause work to be skipped after the owner disappears, which must match the desired behavior.

18. How can a closure create a retain cycle?

A retain cycle occurs when an object strongly owns a closure and that closure strongly captures the same object, directly or through another object. Neither side can lose its last strong owner, so the cycle can remain after external references are removed.

Swift example

final class Counter {
    var count = 0
    var increment: (() -> Void)?

    func configure() {
        increment = { [weak self] in
            self?.count += 1
        }
    }
}

final class Counter {
    var count = 0
    var increment: (() -> Void)?

    func configure() {
        increment = { [weak self] in
            self?.count += 1
        }
    }
}

final class Counter {
    var count = 0
    var increment: (() -> Void)?

    func configure() {
        increment = { [weak self] in
            self?.count += 1
        }
    }
}

The weak capture allows the counter to be released even while its stored closure exists. Another valid design is to clear a one-shot closure after use or arrange ownership so that no cycle exists in the first place.

I draw the ownership graph rather than labeling every closure a leak. A temporary strong capture can be intentional and harmless when the closure has a bounded lifetime. Conversely, a long-running subscription or repeating timer can create a problem even if the original callback looked small.

19. What is the difference between an app lifecycle and a scene lifecycle?

The app lifecycle concerns the application process and shared application-level events. A scene lifecycle concerns one instance of the user interface. A scene-based app can have multiple scenes with different activity states, so one window entering the background does not necessarily mean every window is inactive.

In UIKit, the app delegate still handles application-level setup, while scene delegates handle scene connection and foreground/background transitions. A SwiftUI app expresses its scenes through the App and Scene model and can observe scene phase. Modern design should treat UI state as scene-scoped rather than assume a single global window.

For example, each document window may preserve its own navigation selection while sharing a persistence service. I would not place all foreground logic in historical app-delegate callbacks after enabling scene support, because those callbacks no longer represent each scene's transitions. Lifecycle requirements also depend on the linked SDK, so legacy launch behavior should not be used as a template for a new project.

20. What should an iOS app do when it enters the background?

It should finish only necessary bounded work, preserve important state, and become ready for suspension. Entering the background does not grant unlimited execution time, and the process may later terminate without a convenient final callback. Important user data should therefore be saved as changes occur or at appropriate checkpoints, not only during termination.

For a document editor, I would persist committed changes, stop unnecessary animations or polling, release expendable resources, and prepare the interface for the app-switcher snapshot. Sensitive screens may need an appropriate privacy cover before that snapshot is taken.

Background execution mechanisms exist for specific supported purposes, but a generic timer or detached task does not bypass the system's scheduling rules. When several scenes exist, I also distinguish a single scene becoming inactive from the whole application's needs. The exact work depends on whether the operation is foreground-only, a supported background transfer, or another explicitly authorized background task.

21. What is a UIViewController responsible for?

A UIViewController manages a hierarchy of UIKit views and coordinates that interface's behavior. It responds to lifecycle events, handles presentation and containment, and connects user actions to application logic. It is not supposed to become the application's networking client, database layer, and business-rule engine all at once.

A product-detail controller might configure labels, show a loading indicator, and forward an add-to-cart action to a dependency. Separating fetching and domain rules makes those rules easier to test without constructing a complete view hierarchy. The controller remains responsible for translating state into the appropriate screen presentation.

I also distinguish a view from a view controller: a reusable view renders and handles local interaction, whereas a controller participates in navigation and screen lifecycle. Child controllers need proper containment calls rather than merely adding their views. These boundaries matter because a screen that appears correct can still have broken lifecycle propagation or ownership.

22. How do viewDidLoad, viewWillAppear, and layout callbacks differ?

viewDidLoad runs after a controller's view hierarchy has loaded and is useful for initial configuration. viewWillAppear runs before an appearance transition and may occur repeatedly. Layout callbacks concern arranging the current view geometry and can run many times as sizes or constraints change.

I configure stable view relationships and register reusable cells when the view loads. I refresh appearance-sensitive information when the screen becomes visible if the product requires it. Geometry-dependent work belongs in an appropriate layout phase rather than assuming the initial frame is final in viewDidLoad.

A common bug is adding the same subview or constraint every time the screen appears, causing duplication. Another is starting expensive work during each layout pass. When overriding UIKit lifecycle methods, I preserve required superclass behavior and keep the work bounded. Appearance does not prove a network request finished, and disappearance does not necessarily mean the controller has been deallocated.

23. Why should UI work be isolated to the main actor?

UIKit and much of SwiftUI's UI-facing behavior require main-thread coordination, and MainActor expresses the corresponding isolation in Swift concurrency. Marking UI-owning code with @MainActor lets the compiler check access across isolation boundaries instead of relying solely on comments about which queue should call a method.

Swift example

import UIKit

@MainActor
final class StatusController: UIViewController {
    private let statusLabel = UILabel()

    func showStatus(_ message: String) {
        statusLabel.text = message
    }
}

import UIKit

@MainActor
final class StatusController: UIViewController {
    private let statusLabel = UILabel()

    func showStatus(_ message: String) {
        statusLabel.text = message
    }
}

import UIKit

@MainActor
final class StatusController: UIViewController {
    private let statusLabel = UILabel()

    func showStatus(_ message: String) {
        statusLabel.text = message
    }
}

An asynchronous caller outside this isolation may need await to call the method. That does not mean all work should run on the main actor: large parsing loops, image processing, and blocking I/O can still freeze the interface if performed there.

I distinguish isolation from asynchrony. An async function can suspend, but simply adding async or wrapping work in Task does not guarantee expensive synchronous work moves away from the main actor.

24. What problem does Auto Layout solve?

Auto Layout describes relationships between view positions and sizes using constraints, allowing UIKit to calculate a layout for the current environment. It helps an interface adapt to different screen sizes, orientation changes, safe areas, localization, and larger text without hard-coding every frame.

For a label, I might constrain its leading and trailing edges to the safe-area margins and allow its intrinsic content size to determine height. A fixed height that works for English text can clip a longer translation or a larger accessibility font. Constraints should express the intended relationships rather than reproduce one screenshot numerically.

When creating constrained views in code, I usually disable autoresizing-mask translation for those views and activate a sufficient, nonconflicting constraint set. Ambiguous layout means multiple solutions are possible; unsatisfiable layout means constraints conflict. I inspect the relevant constraints and priorities rather than hide warning messages or add arbitrary offsets until the current device looks acceptable.

25. What are delegates and data sources in UIKit?

A delegate receives behavior-related callbacks or helps make decisions; a data source supplies content and structure. UIKit uses these protocols to let a framework object collaborate with application code without requiring a subclass for every variation.

For a table view, the data source describes the rows and provides their cells, while the delegate can respond to row selection and other interaction or presentation events. A diffable data source is another way to describe collection identity and updates, but the underlying distinction between content and behavior remains useful.

I avoid assuming a delegate is automatically retained. Many UIKit delegate properties are weak, so a separately created delegate or data-source object needs a suitable strong owner. I also keep callbacks quick and free of blocking I/O because many occur during interactive UI work. Protocol conformance establishes that required methods exist; it does not prove that the implementation's state is consistent with what the view displays.

26. Why do table and collection views reuse cells?

Cell reuse limits the number of view objects needed for a long scrolling list. Instead of creating one permanent cell for every record, the view recycles cells as items leave the visible area and reconfigures them for new items. A cell is therefore a temporary presentation of data, not the authoritative storage for that data.

Every configuration path should set the visible state it owns, including labels, images, selection indicators, and visibility. Otherwise an old item's state can appear in a reused cell. An asynchronous image request needs additional care: before applying the result, the code must ensure the cell still represents the requested item, or route the result through stable model identity.

I use prepareForReuse for relevant cleanup and reset work, while still making normal configuration complete. I do not use an index path as a permanent business identifier because insertions and reordering can change it. Stable identifiers make updates and testing easier to reason about.

27. How do push navigation and modal presentation differ in UIKit?

Push navigation adds a screen to a navigation controller's stack, usually representing movement deeper into a hierarchy. Modal presentation introduces a separate task or context over the current interface. The difference is about the user's flow, not merely the animation.

A catalog might push a product-detail screen and offer a back action to the list. A compose-message workflow might be presented modally and expose cancel and send actions. I choose based on the relationship between the screens and preserve the expected way to leave the flow.

The presenting controller and navigation controller also affect ownership and lifecycle. Dismissing a modal is not the same operation as popping a pushed controller. Presentation styles can adapt to device size, so I avoid assuming a modal always covers the full screen. User data should not be lost simply because an interactive dismissal occurs; the flow needs an explicit policy for unsaved changes and completed work.

28. Why use asset catalogs and semantic colors?

Asset catalogs organize resources such as images, app icons, and named colors, including variants appropriate to the current appearance or device characteristics. Semantic system colors describe a role, such as a label or background, and adapt to supported appearance settings more reliably than arbitrary hard-coded RGB values.

For example, a text label using the standard label color can remain readable in both light and dark appearances when placed on a suitable system background. A branded named color can have carefully designed variants rather than simply inverting its components. Image scale and vector configuration also affect sharpness and memory use.

I still test the actual combinations. Using a semantic color does not automatically guarantee sufficient contrast when combined with a custom background, and an icon alone may need an accessibility label. Resource names should be managed consistently so refactoring does not silently leave missing images in a rarely visited screen.

29. How does SwiftUI differ from UIKit?

SwiftUI describes an interface declaratively as a function of data, while UIKit commonly uses objects and imperative updates to configure and change a view hierarchy. Both are native Apple UI frameworks, and an application can integrate them rather than rewrite every screen at once.

In SwiftUI, changing tracked state causes the framework to reevaluate relevant view descriptions. In UIKit, application code often updates a label or applies a snapshot explicitly. This changes how state ownership, identity, and side effects should be designed; translating a controller line for line into a SwiftUI view usually misses the benefit of the model.

I choose between them using deployment requirements, existing code, UI needs, and team experience. SwiftUI does not remove the need to understand accessibility, navigation, asynchronous work, or lifecycle. UIKit is also not inherently obsolete: existing applications and specialized integrations can reasonably use it while adopting SwiftUI in bounded areas.

30. What does the body property of a SwiftUI view represent?

body returns a description of the view's content for the current inputs and tracked state. It is not a one-time screen-construction callback. SwiftUI may evaluate it repeatedly, so it should be inexpensive and should not initiate uncontrolled side effects such as network requests or persistence writes.

Swift example

import SwiftUI

@available(iOS 17.0, *)
struct StockBadge: View {
    let available: Bool

    var body: some View {
        Text(available ? "In stock" : "Unavailable")
            .foregroundStyle(available ? Color.green : Color.secondary)
    }
}

import SwiftUI

@available(iOS 17.0, *)
struct StockBadge: View {
    let available: Bool

    var body: some View {
        Text(available ? "In stock" : "Unavailable")
            .foregroundStyle(available ? Color.green : Color.secondary)
    }
}

import SwiftUI

@available(iOS 17.0, *)
struct StockBadge: View {
    let available: Bool

    var body: some View {
        Text(available ? "In stock" : "Unavailable")
            .foregroundStyle(available ? Color.green : Color.secondary)
    }
}

The view describes how availability affects presentation. SwiftUI decides how to reconcile that description with the rendered interface. some View is an opaque result type: the implementation returns a specific type while callers use the View contract.

I do not use the number of body evaluations as a direct measure of screen redraws or performance. Actual rendering cost and identity changes require measurement, and persistent state belongs in appropriate storage rather than a transient computed property.

This example targets iOS 17 or later, including the Text.foregroundStyle overload. The general View.foregroundStyle modifier is available from iOS 15.

31. What is State used for in SwiftUI?

@State gives a view access to storage that SwiftUI manages for that view's identity. It is appropriate for local mutable UI data, such as whether an explanation is expanded. An ordinary stored variable in a view structure does not provide the same persistent tracked storage.

Swift example

import SwiftUI

struct Explanation: View {
    @State private var expanded = false

    var body: some View {
        VStack {
            Button(expanded ? "Hide details" : "Show details") {
                expanded.toggle()
            }
            if expanded { Text("Delivery takes two business days.") }
        }
    }
}

import SwiftUI

struct Explanation: View {
    @State private var expanded = false

    var body: some View {
        VStack {
            Button(expanded ? "Hide details" : "Show details") {
                expanded.toggle()
            }
            if expanded { Text("Delivery takes two business days.") }
        }
    }
}

import SwiftUI

struct Explanation: View {
    @State private var expanded = false

    var body: some View {
        VStack {
            Button(expanded ? "Hide details" : "Show details") {
                expanded.toggle()
            }
            if expanded { Text("Delivery takes two business days.") }
        }
    }
}

The view's description can be recreated without treating every recreation as a new user state. Changing identity, however, can create new state storage. I keep locally owned state private and avoid storing values that can be cheaply derived from other state. State is also not durable persistence: it does not guarantee that data survives process termination or a new scene.

32. How is Binding different from State?

A binding provides read-and-write access to state owned elsewhere; it is not a second independent source of truth. @State commonly owns local storage, while @Binding lets a child edit that storage through a value supplied by its parent.

Swift example

import SwiftUI

struct SoundOption: View {
    @Binding var enabled: Bool

    var body: some View {
        Toggle("Sound", isOn: $enabled)
    }
}
struct Preferences: View {
    @State private var soundEnabled = true

    var body: some View {
        SoundOption(enabled: $soundEnabled)
    }
}

import SwiftUI

struct SoundOption: View {
    @Binding var enabled: Bool

    var body: some View {
        Toggle("Sound", isOn: $enabled)
    }
}
struct Preferences: View {
    @State private var soundEnabled = true

    var body: some View {
        SoundOption(enabled: $soundEnabled)
    }
}

import SwiftUI

struct SoundOption: View {
    @Binding var enabled: Bool

    var body: some View {
        Toggle("Sound", isOn: $enabled)
    }
}
struct Preferences: View {
    @State private var soundEnabled = true

    var body: some View {
        SoundOption(enabled: $soundEnabled)
    }
}

The parent's projected value creates the binding passed to the child. Copying the incoming value into a new state property would instead create separate state that could drift out of sync.

I use an ordinary immutable input when the child only needs to read data and an action closure when the child should request a business operation rather than directly mutate a property. A binding is convenient, but it is not automatically the right domain boundary.

33. How do Observable models, StateObject, and ObservedObject fit together?

These APIs belong to two related observation approaches. With SwiftUI's Observation support on iOS 17 and later, an @Observable model can have its lifetime managed by an owning view's @State; a consuming view can read the passed model, and @Bindable provides bindings when needed. Observation tracks relevant property reads.

The older Combine-based approach uses a model conforming to ObservableObject, often with @Published properties. An owning view uses @StateObject to preserve the model for its identity, while a receiving view uses @ObservedObject to observe the supplied instance. @StateObject is available from iOS 14.

I choose the approach based on deployment support and the existing model rather than mixing wrappers mechanically. Neither approach makes the model's mutable data automatically safe for concurrent access; UI-facing models still need appropriate isolation. Creating a fresh observable object each time a parent computes its body can also reset state or restart work unexpectedly.

34. What are NavigationStack and NavigationLink used for in SwiftUI?

NavigationStack provides stack-based navigation, and NavigationLink lets a user navigate from the current content to another destination. The stack maintains the relationship between the root and pushed screens, including platform-appropriate back behavior. NavigationStack requires iOS 16 or later.

For a small screen, a link can directly describe its destination. For a more structured flow, a link carries a value, and navigationDestination(for:) maps that value's type to destination content. A path can then represent the stack's navigation state, which helps with controlled navigation and restoration.

I keep route values lightweight and stable instead of putting arbitrary live controller objects into navigation state. Navigation is separate from fetching the destination's data: a route might identify a record that still needs loading or may no longer exist. I also avoid nesting stacks casually, because doing so can create confusing back behavior or independent navigation contexts that do not match the intended flow.

35. What is URLSession used for?

URLSession performs network transfers, including data requests, uploads, and downloads. It manages transport-related behavior through a session configuration and tasks. For a typical API request, application code still needs to construct the request, interpret the response, decode the body, and decide how errors affect the interface.

A successful transfer does not automatically mean a successful business operation. An HTTP 404 or 500 response can arrive with data without becoming a transport error, so I inspect the HTTPURLResponse status before treating the body as a success payload. I also distinguish timeout, cancellation, decoding failure, and a server-reported rejection.

The async data APIs are available on iOS 15 and later; older supported targets may use completion handlers or another compatible approach. HTTPS and normal system trust evaluation should remain the default. I avoid blocking the main thread while waiting for a response and represent loading, empty, success, and failure states explicitly.

36. What are Codable and JSONDecoder?

Codable is the combination of the Encodable and Decodable protocols. It expresses that a type can participate in encoding and decoding; JSONDecoder is a Foundation decoder that turns JSON data into a conforming Swift value. Codable itself is not tied exclusively to JSON.

Swift example

import Foundation

struct Product: Decodable {
    let id: Int
    let name: String
}
let data = Data(#"{"id":7,"name":"Notebook"}"#.utf8)
let product = try JSONDecoder().decode(Product.self, from: data)

import Foundation

struct Product: Decodable {
    let id: Int
    let name: String
}
let data = Data(#"{"id":7,"name":"Notebook"}"#.utf8)
let product = try JSONDecoder().decode(Product.self, from: data)

import Foundation

struct Product: Decodable {
    let id: Int
    let name: String
}
let data = Data(#"{"id":7,"name":"Notebook"}"#.utf8)
let product = try JSONDecoder().decode(Product.self, from: data)

The throwing decode operation can fail when required keys are absent or values have incompatible types. I use CodingKeys or decoding strategies when the wire format's naming differs from the model, and explicit date strategies when the service contract requires them.

Decoded data is not automatically trustworthy domain data. A structurally valid response can still contain an invalid quantity, an unauthorized identifier, or an unsafe URL. Domain validation and authorization remain separate from successful decoding.

37. When should you use UserDefaults, files, or a database?

Use UserDefaults for small preference-style values, files for document or binary content, and an appropriate persistence framework or database for structured records that need querying, relationships, or transactional updates. The right choice depends on the data's size, access pattern, durability requirements, and sensitivity.

A selected theme is a reasonable preference. A downloaded video belongs in file storage with an explicit cache or retention policy. A collection of editable records may benefit from Core Data or SwiftData; SwiftData requires iOS 17 or later. A UI state wrapper alone is not a replacement for these durable stores.

I also decide what should be backed up, what can be recreated, and what must be removed when an account signs out. A cache is not the only copy of irreplaceable user work. Persistence errors must be handled and migrations considered when the data format evolves, even if the first version of the app has only a few fields.

38. Why should credentials go in the Keychain rather than UserDefaults?

The Keychain is the platform facility intended for small secrets such as credentials and tokens. UserDefaults is a preferences store and should not be treated as protected credential storage. Choosing the Keychain still requires a deliberate access policy appropriate to when and how the app uses the secret.

For example, a token needed only while the device is unlocked can use a more restrictive accessibility class than data required by an authorized background workflow. Some access controls can require user presence, which changes the user experience and whether unattended access is possible. The policy must fit the threat model rather than simply selecting the strongest-looking option.

I avoid embedding server secrets in an application binary, logging tokens, or assuming local storage makes a compromised session harmless. Tokens should have appropriate server-side lifetimes and revocation behavior. On sign-out, the app should remove the relevant locally stored credentials and stop using any in-memory copies.

39. What makes a basic iOS interface accessible?

An accessible interface exposes understandable semantics, supports assistive technologies, and remains usable when text size, contrast, or input method changes. Standard controls provide a useful starting point, but custom content still needs appropriate labels, values, traits, focus order, and actions.

For a favorite button, I would expose its purpose and current selected state rather than announce only an image filename. Dynamic Type support should allow text to grow without clipping essential information. Color should not be the only way to communicate an error or selection, and motion should respect relevant accessibility preferences.

I test with VoiceOver and large text sizes instead of assuming visual correctness proves accessibility. Decorative images may need to be hidden from the accessibility tree, while meaningful images need a concise description. Accessibility identifiers used by UI tests are not substitutes for user-facing accessibility labels. Good accessibility often improves the same interface for people using it in difficult lighting or with temporary limitations.

40. What is the difference between unit tests and UI tests in iOS?

Unit tests check focused behavior, such as a validation rule, decoder, or state transition, while UI tests exercise the application through its interface. Unit tests are usually faster and easier to make deterministic; UI tests provide confidence that important components work together in a user-visible flow.

I would test a quantity parser with valid, empty, negative, and malformed input without launching a screen. A UI test could then check that entering an invalid quantity displays an error and prevents checkout. Injected dependencies help avoid relying on an uncontrolled live server for either kind of test.

Swift Testing is available with modern Swift toolchains for Swift tests, while XCTest remains relevant, including UI automation through XCUIAutomation. I follow the project's supported toolchain rather than assume one framework replaces every testing task. Passing tests are evidence for the cases they exercise, not proof of complete correctness; boundary cases, accessibility, device behavior, and integration still need deliberate coverage.

iOS Interview Questions and Answers for Intermediate

The intermediate answers focus on correctness at framework and concurrency boundaries. They explain practical code decisions, supported-platform requirements, and the failure paths that an implementation must handle.

41. How do generics, opaque types, and existential types differ in Swift?

Generics preserve a concrete type relationship while allowing callers to supply different conforming types. An opaque result such as some Sequence hides the concrete type from callers but preserves one underlying type chosen by the implementation. An existential such as any Sequence packages a value whose concrete type can vary, subject to the operations available through that interface.

Swift example

func firstValue<C: Collection>(_ values: C) -> C.Element? {
    values.first
}

func identifiers() -> some Collection<Int> {
    [10, 20, 30]
}

func firstValue<C: Collection>(_ values: C) -> C.Element? {
    values.first
}

func identifiers() -> some Collection<Int> {
    [10, 20, 30]
}

func firstValue<C: Collection>(_ values: C) -> C.Element? {
    values.first
}

func identifiers() -> some Collection<Int> {
    [10, 20, 30]
}

The generic function preserves the relationship between its collection and element types. The opaque result hides the array implementation while exposing a collection of integers; primary associated type syntax requires a compatible Swift compiler.

I use generics when relationships between types matter, opaque types when an API should hide one implementation, and existentials when runtime heterogeneity is genuinely useful. any is not automatically a performance problem, but boxing, indirect calls, and lost type relationships can matter in measured hot paths. Neither spelling is an iOS UI feature; compiler support and the referenced SDK APIs must be checked separately.

42. Why can a protocol extension method behave differently from a protocol requirement?

A protocol requirement participates in a conforming type's witness table, while a method declared only in a protocol extension is not a requirement. That difference affects which implementation is selected through a generic constraint or existential interface.

Swift example

protocol Describing {
    func requiredText() -> String
}
extension Describing {
    func requiredText() -> String { "default" }
    func extraText() -> String { "extension" }
}
struct Item: Describing {
    func requiredText() -> String { "item" }
    func extraText() -> String { "concrete" }
}
let value: any Describing = Item()
// value.requiredText() is "item"; value.extraText() is "extension".

protocol Describing {
    func requiredText() -> String
}
extension Describing {
    func requiredText() -> String { "default" }
    func extraText() -> String { "extension" }
}
struct Item: Describing {
    func requiredText() -> String { "item" }
    func extraText() -> String { "concrete" }
}
let value: any Describing = Item()
// value.requiredText() is "item"; value.extraText() is "extension".

protocol Describing {
    func requiredText() -> String
}
extension Describing {
    func requiredText() -> String { "default" }
    func extraText() -> String { "extension" }
}
struct Item: Describing {
    func requiredText() -> String { "item" }
    func extraText() -> String { "concrete" }
}
let value: any Describing = Item()
// value.requiredText() is "item"; value.extraText() is "extension".

If customization must be observed through the protocol, I declare the operation as a requirement and optionally provide a default implementation. Otherwise callers may see an extension implementation even when the concrete type defines a same-named method.

Class inheritance adds another subtlety: a subclass method does not necessarily replace a default witness selected for its superclass's conformance. I test calls through the exact interface used in production rather than assuming every same-named method behaves like an overridden class method.

43. How do you choose a closure capture strategy without creating a retain cycle?

I draw the ownership chain first. A cycle occurs when an object strongly retains a closure that strongly captures that object, directly or through another owner. A weak capture can break that cycle when the work should not keep the object alive. An unowned capture is appropriate only when the lifetime relationship guarantees the object still exists whenever the closure runs.

Swift example

final class Editor {
    var onSave: (() -> Void)?
    var saved = false

    func configure() {
        onSave = { [weak self] in
            self?.saved = true
        }
    }
}

final class Editor {
    var onSave: (() -> Void)?
    var saved = false

    func configure() {
        onSave = { [weak self] in
            self?.saved = true
        }
    }
}

final class Editor {
    var onSave: (() -> Void)?
    var saved = false

    func configure() {
        onSave = { [weak self] in
            self?.saved = true
        }
    }
}

Here the editor owns its callback, so a strong capture would create a cycle. By contrast, a short-lived completion closure may intentionally retain an owner until an operation completes. Weak captures are not a universal rule.

I also inspect nested closures and long-running tasks. Promoting a weak reference to a strong local before a long suspension retains it across that suspension. Cancellation and explicit teardown can be necessary even after a capture cycle has been removed.

44. How would you investigate a view controller that does not deallocate?

I first reproduce a complete presentation and dismissal cycle, then check whether the controller remains alive after expected asynchronous work finishes. A temporary deinit log is a useful signal, but the Memory Graph debugger is better evidence of which objects retain it.

I inspect callbacks stored on the controller, timers, display links, notification observation tokens, subscriptions, child controllers, and task handles. A retained controller may be intentional, such as a navigation controller keeping it on its stack, rather than an ARC defect. I identify the actual retaining path before adding weak references.

A common fix is to invalidate a repeating timer or cancel a subscription when its ownership scope ends. Another is breaking a stored callback cycle. Canceling a task does not instantly release all captured values: its operation must observe cancellation and finish.

I then repeat the flow several times and compare live object counts and memory trends. A flat process-memory graph alone does not prove there is no leak because allocators retain reusable memory. The stronger check is that obsolete controller instances and their expensive owned resources become unreachable.

45. What is the correct way to embed a child view controller?

I establish UIKit containment as well as the view hierarchy. Adding only the child's view misses the parent-child relationship that UIKit uses for lifecycle and other controller behavior. The parent calls addChild, installs and constrains the view, then calls the child's didMove(toParent:).

Swift example

// Inside the parent UIViewController; containerView already exists.
addChild(child)
child.view.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(child.view)
NSLayoutConstraint.activate([
    child.view.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
    child.view.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
    child.view.topAnchor.constraint(equalTo: containerView.topAnchor),
    child.view.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
])
child.didMove(toParent: self)

// Inside the parent UIViewController; containerView already exists.
addChild(child)
child.view.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(child.view)
NSLayoutConstraint.activate([
    child.view.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
    child.view.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
    child.view.topAnchor.constraint(equalTo: containerView.topAnchor),
    child.view.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
])
child.didMove(toParent: self)

// Inside the parent UIViewController; containerView already exists.
addChild(child)
child.view.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(child.view)
NSLayoutConstraint.activate([
    child.view.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
    child.view.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
    child.view.topAnchor.constraint(equalTo: containerView.topAnchor),
    child.view.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
])
child.didMove(toParent: self)

For removal, I call willMove(toParent: nil), remove constraints or other resources owned by the container as needed, remove the child's view, and call removeFromParent(). UIKit completes the corresponding removal notification.

A custom container must also decide how appearance forwarding and supported orientations behave; sophisticated transitions need additional coordination. I prefer standard navigation, tab, and split controllers when their behavior fits, because custom containment is a responsibility, not merely a way to organize files.

46. How do you prevent reused cells from showing an incorrect downloaded image?

I associate each asynchronous result with the model identity it was requested for, not just the cell object or its old index path. A cell can represent a different item by the time an image finishes downloading. Configuration should immediately install a placeholder and reset every state that depends on the previous item.

I store the represented item's stable identifier on the cell, cancel obsolete work during reconfiguration or reuse, and compare the identifier again before applying a result on the UI isolation domain. Cancellation alone is insufficient because the operation may already have completed or may not stop immediately. An index path is also insufficient because inserts and moves can change it.

prepareForReuse() is useful for releasing subscriptions and resetting transient presentation, but the normal configuration path must be complete. Some reconfiguration APIs update existing cells without invoking a reuse cycle.

For a scrolling list, I also coalesce equivalent image requests, cache appropriately sized decoded images, and avoid expensive image decoding in UI-critical work. These optimizations follow correctness: a fast list that displays another person's avatar is still broken.

47. How does a diffable data source update a collection view reliably?

A diffable data source compares snapshots of stable section and item identifiers to derive insertions, deletions, and moves. I update the model store, construct the desired identifier snapshot, and apply it instead of manually reconciling index-path arithmetic.

Identifiers must be unique within the snapshot and stable for the lifetime of the logical item. I normally use a persistent ID rather than a hash of mutable display fields. Two different IDs may have the same hash; equality, not collision-free hashing, determines identity.

Changing an item's title without changing its identifier does not automatically tell the data source that its content changed. I explicitly reconfigure or reload the affected identifiers and ensure the cell provider reads the updated model. reconfigureItems requires iOS 15 or later; diffable data sources themselves arrived in iOS 13.

I serialize snapshot updates in a consistent execution context and avoid applying snapshots that refer to records missing from the model store. For rapidly arriving changes, coalescing updates can reduce animation churn. Diffable data sources simplify UI synchronization, but they do not observe the underlying business data for me.

48. What makes a self-sizing table or collection cell work correctly?

A self-sizing cell needs enough layout information to determine its content size at the actual available width. For Auto Layout table cells, that normally means an unbroken chain of constraints from the content view's top to bottom, meaningful horizontal constraints, and appropriate intrinsic sizes or explicit dimensions.

I use multiline labels where needed, avoid fixed heights that conflict with larger text, and configure automatic row sizing with a reasonable estimate. For collection views, I choose a layout that supports estimated dimensions and let the final size reflect the content. An estimate is a starting point, not a promise that every item has that height.

When sizing fails, I examine ambiguous or conflicting constraints, content hugging and compression resistance, image aspect ratios, and whether asynchronous content triggers a layout update. I also check reuse: a hidden label or old constraint from a previous item can distort measurement.

Tests should include the longest localized strings, narrow windows, right-to-left content, and accessibility text sizes. A cell that fits an English sample at the default font size has not demonstrated reliable self-sizing.

49. How does SwiftUI view identity affect state and task lifetime?

SwiftUI associates persistent state with a view's identity in the hierarchy, not with the temporary value of a view struct. Recomputing body does not by itself create a new logical screen or reset its state. Changing structural position, view type, or an explicit identity can change that association.

For lists, I use stable model identifiers. Generating a new UUID during every render or identifying mutable rows by their array offsets can cause state to move to the wrong item or be discarded. An explicit .id can deliberately reset a subtree, but using it to force refreshes often hides a modeling problem.

Identity also matters for work started through view modifiers. A task tied to a view can be canceled when the view leaves; a task with an ID restarts when that ID changes. Cancellation remains cooperative, so old work must still avoid publishing an obsolete result.

I keep durable business state in an appropriate owner outside a transient row, and view-local interaction state near the view. The right lifetime follows product behavior, not simply which property wrapper makes the compiler accept the code.

50. How does Observation change SwiftUI model updates?

With Observation, SwiftUI tracks observable properties read while evaluating a view and updates that view when those dependencies change. This is more precise than treating every published change on an observed object as relevant to every subscribing view. SwiftUI's Observation integration requires iOS 17 or later.

Swift example

import SwiftUI
import Observation

@MainActor @Observable
final class FilterModel {
    var query = ""
    var isEnabled = true
}

@MainActor
struct FilterEditor: View {
    @Bindable var model: FilterModel
    var body: some View {
        TextField("Filter", text: $model.query)
    }
}

import SwiftUI
import Observation

@MainActor @Observable
final class FilterModel {
    var query = ""
    var isEnabled = true
}

@MainActor
struct FilterEditor: View {
    @Bindable var model: FilterModel
    var body: some View {
        TextField("Filter", text: $model.query)
    }
}

import SwiftUI
import Observation

@MainActor @Observable
final class FilterModel {
    var query = ""
    var isEnabled = true
}

@MainActor
struct FilterEditor: View {
    @Bindable var model: FilterModel
    var body: some View {
        TextField("Filter", text: $model.query)
    }
}

@Bindable supplies bindings for an existing observable model; it does not create a new ownership scope. A view that creates and owns such a model can use @State, while a view that only reads an injected model may use an ordinary property. Observation itself does not provide thread safety, which is why this UI model has explicit main-actor isolation.

For earlier deployment targets, ObservableObject, @Published, @StateObject, and @ObservedObject remain valid tools. I migrate ownership and bindings deliberately rather than mechanically replacing annotations.

51. How do you design SwiftUI bindings without creating competing sources of truth?

A binding is a read-write connection to state owned elsewhere, not a second independent copy of that state. I identify the authoritative owner first, then pass values for read-only dependencies and bindings only where the child should edit the original value.

For a settings toggle, a binding directly to the setting may be appropriate. For an editor with Save and Cancel, I often create a deliberate draft value and commit it only on Save. That draft is not accidental duplicated state: it has a clear transaction boundary and a defined synchronization policy.

With ObservableObject, an owning view generally uses @StateObject and a borrowing view uses @ObservedObject. With an Observation model on iOS 17+, the owning view can use @State, and @Bindable exposes writable properties when needed. These choices are not interchangeable merely because they all trigger UI updates.

Custom bindings should have predictable getters and setters. A getter that performs network requests or a setter that creates circular updates makes rendering fragile. I keep validation and business rules in the model and represent rejected edits or errors explicitly instead of silently fighting the control's state.

52. How do you implement programmatic navigation with NavigationStack?

I represent navigation as route data and bind a NavigationStack to that path. Appending a route pushes a destination; removing routes pops screens. This makes navigation testable and gives deep links and restoration a state model rather than a chain of imperative view operations. NavigationStack requires iOS 16 or later.

Swift example

import SwiftUI

enum Route: Hashable { case order(Int) }

struct OrdersRoot: View {
    @State private var path: [Route] = []
    var body: some View {
        NavigationStack(path: $path) {
            Button("Open order") { path.append(.order(42)) }
                .navigationDestination(for: Route.self) { route in
                    switch route {
                    case .order(let id): Text("Order \(id)")
                    }
                }
        }
    }
}

import SwiftUI

enum Route: Hashable { case order(Int) }

struct OrdersRoot: View {
    @State private var path: [Route] = []
    var body: some View {
        NavigationStack(path: $path) {
            Button("Open order") { path.append(.order(42)) }
                .navigationDestination(for: Route.self) { route in
                    switch route {
                    case .order(let id): Text("Order \(id)")
                    }
                }
        }
    }
}

import SwiftUI

enum Route: Hashable { case order(Int) }

struct OrdersRoot: View {
    @State private var path: [Route] = []
    var body: some View {
        NavigationStack(path: $path) {
            Button("Open order") { path.append(.order(42)) }
                .navigationDestination(for: Route.self) { route in
                    switch route {
                    case .order(let id): Text("Order \(id)")
                    }
                }
        }
    }
}

I store lightweight route identifiers instead of mutable model objects. Destination screens resolve current data and handle missing or unauthorized records. An array works well for one route enum; NavigationPath supports heterogeneous values when that is useful.

Path restoration requires versioning and validation, not blindly decoding old state. Modal presentation and selected tabs may be separate state, and each independent window should own the navigation state appropriate to that scene.

53. How should UIViewRepresentable coordinate UIKit and SwiftUI state?

A representable creates a UIKit view once for a given identity and updates it when SwiftUI inputs change. I put construction and initial configuration in makeUIView, and synchronize current input values in updateUIView. A coordinator can bridge delegate callbacks back into bindings or other explicit actions.

The main risk is a feedback loop: SwiftUI updates a UIKit property, the delegate treats it as a user edit, and the binding writes back repeatedly. I compare old and new values and distinguish programmatic updates from genuine user events where the UIKit API requires it. I also avoid mutating SwiftUI state synchronously as a side effect of evaluating an update when that would trigger an invalid nested update.

dismantleUIView is the place to remove resources tied to the represented view, such as delegate connections or observers that the wrapper installed. A coordinator is not a permanent copy of the initial representable value; callbacks must use up-to-date dependencies.

For the opposite direction, UIHostingController embeds SwiftUI in UIKit and still needs correct controller containment. Interoperability succeeds when ownership and lifecycle are explicit, not when both frameworks independently control the same state.

54. How do SwiftUI layout proposals differ from Auto Layout constraints?

SwiftUI layout generally works through a proposal-and-response process: a parent proposes a size, a child chooses a size, and the parent places it. It is not a global system of Auto Layout equations. Modifiers wrap views and can change what is proposed, reported, or drawn, so their order matters.

For example, padding before a background includes that padding inside the colored area, while padding after the background leaves outside spacing. A fixed frame is another layout layer rather than an instruction that always changes the intrinsic behavior of every descendant. layoutPriority helps resolve competition for space, but it is not equivalent to a universal width constraint.

I first inspect which container proposes the available space and which child refuses or expands within it. I prefer stacks, grids, alignment guides, and appropriate layout primitives before reaching for a geometry reader that can introduce unwanted expansion.

A custom Layout implementation, available from iOS 16, can measure and place subviews for reusable arrangements. I test rotation, localization, text scaling, and narrow containers; a preview of one device width does not validate the layout contract.

55. What does MainActor guarantee, and what does async not guarantee?

MainActor expresses an isolation domain for UI-related state. Calls from outside that domain may need an asynchronous hop before accessing isolated operations. On Apple platforms its executor integrates with the main thread, but the useful programming guarantee is isolation, not arbitrary thread checks in every function.

An async function can suspend; it does not automatically move expensive synchronous work to a background thread. Putting a large JSON transformation inside a main-actor task can still block UI responsiveness. Conversely, waiting for a properly asynchronous network operation does not block the actor for the entire request.

Swift example

@MainActor
final class ScreenState {
    private(set) var title = "Loading"
    func show(_ value: String) { title = value }
}

@MainActor
final class ScreenState {
    private(set) var title = "Loading"
    func show(_ value: String) { title = value }
}

@MainActor
final class ScreenState {
    private(set) var title = "Loading"
    func show(_ value: String) { title = value }
}

I annotate the state owner rather than scattering dispatch calls around individual assignments. In Swift 6.2, build settings such as default actor isolation and NonisolatedNonsendingByDefault affect inferred isolation and nonisolated async execution. @concurrent can explicitly request execution away from the caller's actor in that model. I check the project's compiler settings instead of assuming a generic rule based only on the presence of async.

56. Why can an actor still have a race in its business logic?

An actor prevents simultaneous unsynchronized access to its isolated mutable state, but an await allows other work to run before the suspended operation resumes. State assumptions made before suspension may no longer be true afterward. This is actor reentrancy, and it can produce logical races without a memory data race.

Suppose an inventory actor checks that one ticket remains, awaits a payment authorization, then decrements stock. Another request can enter while payment is pending and pass the same initial check. Both requests can then believe they bought the final ticket.

I solve that by modeling the operation explicitly: reserve inventory before suspension, record an operation identifier, and either commit or release the reservation afterward. Alternatively I revalidate a version or condition after the await when retrying is safe. Server-side transactional guarantees remain necessary if several clients share the resource.

I do not hold a blocking lock across an await to simulate a transaction. Instead, I keep each uninterrupted actor segment internally consistent and make transitions between segments visible in the state machine. Tests should deliberately control completion order, not rely on random scheduling to expose the bug.

57. When should you use Task, Task.detached, or a structured child task?

I prefer structured child tasks when concurrent work belongs to a parent operation because their lifetime, cancellation relationship, and completion are bounded by the surrounding scope. async let and task groups express that relationship. Task { ... } creates an unstructured task, useful for entering asynchronous code from a synchronous event handler, but its owner must manage any required cancellation and lifetime.

A task created with Task can inherit actor isolation and task-local context from where it is created. Task.detached does not inherit that actor context and is not a general-purpose fix for concurrency diagnostics. Detached work needs explicit input ownership, safe transfers, and a deliberate completion policy.

For a screen load, I normally use a SwiftUI task modifier or store a task handle in the controller's owner and cancel it when superseded. For dependent suboperations, I use structured concurrency inside that task.

Neither unstructured nor detached tasks automatically become a durable background job that survives app suspension or termination. iOS background execution is a separate platform concern. I also avoid fire-and-forget tasks for work whose error must affect a user-visible transaction.

58. When is async let preferable to sequential awaits?

I use async let when a small, known set of independent operations can run concurrently and all belong to the same scope. Sequential awaits are better when the second operation depends on the first result, or when ordering and resource limits are intentional.

Swift example

func loadPair(
    left: @Sendable () async throws -> Int,
    right: @Sendable () async throws -> Int
) async throws -> (Int, Int) {
    async let a = left()
    async let b = right()
    return try await (a, b)
}

func loadPair(
    left: @Sendable () async throws -> Int,
    right: @Sendable () async throws -> Int
) async throws -> (Int, Int) {
    async let a = left()
    async let b = right()
    return try await (a, b)
}

func loadPair(
    left: @Sendable () async throws -> Int,
    right: @Sendable () async throws -> Int
) async throws -> (Int, Int) {
    async let a = left()
    async let b = right()
    return try await (a, b)
}

The declarations start child tasks without waiting for each result immediately. Awaiting the tuple obtains the results. Leaving the scope cannot leave those child tasks running indefinitely as detached work; outstanding children are canceled and awaited as required by structured concurrency.

Concurrency does not guarantee parallel execution or lower latency. The operations might compete for one actor or saturate a constrained service. I also decide how failure should affect the combined result: a dashboard might tolerate an unavailable optional panel, while a transaction may require every component. That policy should be encoded in return values and error handling rather than accidentally determined by whichever await throws first.

59. How do task groups handle a dynamic number of concurrent operations?

Task groups let a parent create and collect a runtime-dependent set of child tasks. Results arrive in completion order, not necessarily input order. If order matters, each child should return an index or stable identifier so the parent can reconstruct the desired arrangement.

I avoid adding thousands of expensive tasks at once. A bounded pattern seeds the group with a limited number of children, consumes one result, then adds the next pending item. This limits pressure on memory, network connections, and downstream services. The correct limit depends on the actual workload, not the number of CPU cores alone.

A throwing task group needs an explicit failure policy. If an error escapes the group body, remaining work is canceled and the scope waits for children to finish. Canceling does not forcibly terminate a child; the operation must cooperate. If partial success is acceptable, children can return a result value that represents success or failure instead of aborting the whole operation.

Only the group owner manipulates the group itself. Child tasks should return safe values rather than mutate a shared collection without isolation. That keeps both aggregation and cancellation behavior understandable.

60. How do you implement cooperative cancellation correctly?

Cancellation is a request, not a forced interruption. I propagate it to underlying operations where possible, check it before expensive stages, and stop publishing results once the operation is no longer relevant. APIs that throw for cancellation should normally let that signal propagate rather than present it as a user-facing failure.

Swift example

func sumSquares(_ values: [Int]) async throws -> Int {
    var total = 0
    for (index, value) in values.enumerated() {
        if index.isMultiple(of: 256) {
            try Task.checkCancellation()
        }
        total += value * value
    }
    try Task.checkCancellation()
    return total
}

func sumSquares(_ values: [Int]) async throws -> Int {
    var total = 0
    for (index, value) in values.enumerated() {
        if index.isMultiple(of: 256) {
            try Task.checkCancellation()
        }
        total += value * value
    }
    try Task.checkCancellation()
    return total
}

func sumSquares(_ values: [Int]) async throws -> Int {
    var total = 0
    for (index, value) in values.enumerated() {
        if index.isMultiple(of: 256) {
            try Task.checkCancellation()
        }
        total += value * value
    }
    try Task.checkCancellation()
    return total
}

This bounded integer example assumes values are small enough to avoid overflow. It demonstrates cancellation polling, not a guarantee of background execution. A CPU-heavy caller must separately choose an appropriate execution context.

Cancellation handlers can signal a callback-based resource to stop, but their synchronization and ordering require care. I also guard against stale results with request identity because cancellation may race with completion. For UI search, canceling the previous task and checking the active query version address different problems; both may be needed.

61. What does Sendable mean, and when is unchecked conformance dangerous?

Sendable describes values that can safely cross concurrency isolation boundaries. Immutable value data composed of sendable properties is a natural fit. Shared mutable reference state needs a real synchronization or isolation strategy; adding a marker does not create one.

Swift example

struct SearchResult: Sendable {
    let id: Int
    let title: String
}

struct SearchResult: Sendable {
    let id: Int
    let title: String
}

struct SearchResult: Sendable {
    let id: Int
    let title: String
}

The compiler can verify this simple value type. A class wrapping mutable dictionaries and callbacks is a different case. I first consider actor isolation, immutable snapshots, or keeping the object within one domain rather than making it transferable.

@unchecked Sendable tells the compiler to trust my implementation. It is justified only when every relevant access is protected by a mechanism the compiler cannot prove, with a documented invariant and tests. It is not a routine migration shortcut. Likewise, @Sendable closure requirements constrain what the closure may safely capture; they do not mean the closure always executes on a particular queue.

Swift 6 language mode strengthens data-race diagnostics, while SDK annotations and compiler isolation settings also affect checking. I address the ownership boundary the diagnostic exposes instead of suppressing it until the build turns green.

62. What are the rules for wrapping a callback API with a continuation?

A continuation bridges a callback completion into an async function, and every execution path must resume it exactly once. Missing a resume leaves the caller suspended; resuming twice is a programming error. A checked continuation helps diagnose misuse, but it does not invent a missing cancellation contract.

Swift example

func loadNumber(
    start: (@escaping (Result<Int, Error>) -> Void) -> Void
) async throws -> Int {
    try await withCheckedThrowingContinuation { continuation in
        start { result in
            continuation.resume(with: result)
        }
    }
}

func loadNumber(
    start: (@escaping (Result<Int, Error>) -> Void) -> Void
) async throws -> Int {
    try await withCheckedThrowingContinuation { continuation in
        start { result in
            continuation.resume(with: result)
        }
    }
}

func loadNumber(
    start: (@escaping (Result<Int, Error>) -> Void) -> Void
) async throws -> Int {
    try await withCheckedThrowingContinuation { continuation in
        start { result in
            continuation.resume(with: result)
        }
    }
}

This wrapper assumes start invokes its callback exactly once. If the original API can deliver several events, an async sequence is more appropriate. If it can report completion concurrently with cancellation, I need a synchronized state machine that selects one terminal outcome and prevents double resume.

I also retain any underlying operation token for as long as needed and decide what task cancellation should do to it. Simply wrapping a request in a continuation does not make the request cancelable. Callback executor behavior and isolation of captured values must satisfy the project's Swift concurrency checks.

63. When would you use AsyncSequence or AsyncStream?

An async sequence models a series of values that may arrive over time, while an async function normally returns one result. I use for await or for try await to consume notifications, progress, or streamed data without expressing every event as a nested callback.

AsyncStream can adapt a callback source that emits multiple values. The adapter should define buffering, finish the stream when the source ends, and use termination handling to release the source subscription when consumption ends. For high-frequency events, an unbounded buffer can become a memory problem; a latest-value UI signal may reasonably discard older pending updates, while an audit log cannot.

I do not assume that yielding into an AsyncStream suspends the producer until the consumer catches up. A buffer policy is not the same as end-to-end backpressure. If lossless flow control is required, the producer protocol or a suitable channel abstraction must support it.

Cancellation behavior belongs to the specific sequence and adapter. I test early loop exit, task cancellation, source failure, and normal completion, and avoid retaining an owner indefinitely through a stream continuation or subscription.

64. How do you validate a URLSession response before decoding it?

I distinguish transport failure from HTTP failure and decoding failure. A completed URL session request can return a valid HTTP response with a 404 or 500 status without throwing a transport error. I validate the response contract before decoding the expected success model.

Swift example

import Foundation

enum APIError: Error { case invalidResponse; case status(Int) }
struct Summary: Decodable { let count: Int }

func fetchSummary(from url: URL, using session: URLSession) async throws -> Summary {
    let (data, response) = try await session.data(from: url)
    guard let http = response as? HTTPURLResponse else {
        throw APIError.invalidResponse
    }
    guard (200..<300).contains(http.statusCode) else {
        throw APIError.status(http.statusCode)
    }
    return try JSONDecoder().decode(Summary.self, from: data)
}

import Foundation

enum APIError: Error { case invalidResponse; case status(Int) }
struct Summary: Decodable { let count: Int }

func fetchSummary(from url: URL, using session: URLSession) async throws -> Summary {
    let (data, response) = try await session.data(from: url)
    guard let http = response as? HTTPURLResponse else {
        throw APIError.invalidResponse
    }
    guard (200..<300).contains(http.statusCode) else {
        throw APIError.status(http.statusCode)
    }
    return try JSONDecoder().decode(Summary.self, from: data)
}

import Foundation

enum APIError: Error { case invalidResponse; case status(Int) }
struct Summary: Decodable { let count: Int }

func fetchSummary(from url: URL, using session: URLSession) async throws -> Summary {
    let (data, response) = try await session.data(from: url)
    guard let http = response as? HTTPURLResponse else {
        throw APIError.invalidResponse
    }
    guard (200..<300).contains(http.statusCode) else {
        throw APIError.status(http.statusCode)
    }
    return try JSONDecoder().decode(Summary.self, from: data)
}

The async URLSession method requires iOS 15 or later. Production code should follow the endpoint's exact status and payload contract: a 204 response does not contain this summary JSON, for example. I also consider content type, payload size, redacted diagnostics, authentication, and cancellation. Those checks belong in a reusable transport boundary rather than being inconsistently copied across screens.

65. How do you make Codable decoding resilient without hiding schema errors?

I model the API contract explicitly and distinguish optional data from malformed required data. An optional property allows a missing or null value when that is valid; it does not make a wrong type acceptable. Broadly catching every decoding error and substituting defaults can turn corrupted responses into plausible but incorrect UI.

For evolving payloads, I use CodingKeys for deliberate name mapping, explicit date and key strategies, and custom decoding only where the contract calls for it. Unknown JSON fields are usually harmless to synthesized decoding, but unknown enum cases may need an explicit fallback policy.

I inspect DecodingError and its coding path in diagnostics, redacting the actual payload where it may contain personal information. A message such as “missing field at results[2].price” is often more useful and safer than logging the entire server response.

Tests should include minimal valid data, nulls, missing required fields, unexpected types, new enum values, and date-format variations. I keep transport DTOs separate from domain models when server quirks would otherwise contaminate application invariants. Resilience means deliberate compatibility, not accepting every shape as success.

66. How do HTTP caching and an in-memory model cache differ?

HTTP caching reuses representations according to request policy and server metadata such as freshness directives and validators. URLCache integrates with URL loading for that purpose. A model cache stores application-level objects and follows policies the application defines, such as account scope, normalization, and stale-while-refresh behavior.

I do not treat a dictionary of decoded responses as a replacement for HTTP semantics. A response may require revalidation or forbid storage, and an application cache must respect privacy and authorization boundaries. Conversely, HTTP caching does not automatically merge several endpoint responses into a consistent domain object graph.

For a profile screen, I might display an account-scoped stored model while refreshing through a properly configured session. I track freshness separately from loading state and clear or segregate cached data when the user changes accounts. A stale response should not overwrite a newer edit merely because it arrived last.

Testing covers cache hits, conditional requests, offline behavior, logout, and resource changes. I avoid disabling caching globally to fix one stale screen; the right fix may be an invalidation rule, incorrect server headers, or a UI update bug.

67. When is retrying a failed network request safe?

A retry is safe only when the operation's semantics and the failure mode make repeating it acceptable. Reads are often retryable; a payment or order submission may have succeeded on the server even if the client never received the response. For mutations, I prefer a server-supported idempotency mechanism or a way to query the operation's outcome.

I retry selected transient failures with a bounded attempt count, backoff, and jitter. I respect server retry guidance when applicable and stop on cancellation. Authentication failures, validation errors, and permanent resource errors generally need a different response rather than a blind retry loop.

The retry budget should include total elapsed time and the user's current intent. An old search request should not keep retrying after the query changes. A connectivity monitor is useful for experience and diagnostics, but it cannot prove that a particular request will succeed; the request remains the authoritative test.

I inject delay and transport dependencies so tests can simulate timeouts, duplicate responses, and eventual success without sleeping in real time. This keeps retry logic testable and prevents hidden traffic amplification during an outage.

68. How do you use Core Data safely from background work?

I keep managed objects within their managed object context's concurrency domain. A background context performs its work through its own perform APIs; I do not pass a live managed object from the view context into an arbitrary task or queue. To refer to an existing record elsewhere, I pass its object ID and resolve it in the receiving context.

The persistent container's view context normally serves UI work, while performBackgroundTask or a background context can import and save data without doing that work directly on the UI context. Saving a background context does not mean every displayed value automatically updates under every configuration. I configure merging or process save changes according to the application's needs.

Temporary object IDs require care when transferring references before a save; the receiving context needs a usable persistent identity. Deleted records and merge conflicts also need explicit handling.

For large imports, I batch work, control memory, and measure save cost. Swift tasks are not substitutes for Core Data's context rules. Even in an actor-based repository, the repository must execute managed-object access through the context's supported concurrency APIs.

69. How should you plan a SwiftData schema migration?

I treat persistent schema changes as a compatibility problem for existing user data, not just a source-code refactor. SwiftData is available from iOS 17. I identify every released schema that users may still have, define versioned schemas where appropriate, and use a migration plan with suitable lightweight or custom stages.

A new optional property may be straightforward, while splitting one entity, changing uniqueness assumptions, or transforming stored values can require a carefully staged migration. Renaming a Swift property should preserve its intended storage identity rather than accidentally deleting old information and creating an empty replacement.

I test migration using copies of stores created by older app versions, including empty stores, realistic large stores, duplicates, missing values, and interrupted upgrade conditions. Testing only a fresh installation says nothing about upgrade safety.

The app should handle store-opening failure without silently deleting the user's data. Recovery options, diagnostics, backups or export where relevant, and product messaging deserve explicit design. SwiftData's convenient model declarations do not remove the need to understand persistence, context ownership, and rollback risks before shipping a schema change.

70. How does dependency injection make an iOS feature easier to test?

Dependency injection gives a feature its collaborators from the outside instead of letting it create hidden global services. A screen model can receive a repository, clock, or identifier generator through its initializer. Tests then provide controlled implementations that make success, failure, cancellation, and timing reproducible.

Swift example

protocol PriceProviding: Sendable {
    func price() async throws -> Int
}
struct FixedPrice: PriceProviding {
    let amount: Int
    func price() async throws -> Int { amount }
}

protocol PriceProviding: Sendable {
    func price() async throws -> Int
}
struct FixedPrice: PriceProviding {
    let amount: Int
    func price() async throws -> Int { amount }
}

protocol PriceProviding: Sendable {
    func price() async throws -> Int
}
struct FixedPrice: PriceProviding {
    let amount: Int
    func price() async throws -> Int { amount }
}

The real implementation can use networking, while a test supplies FixedPrice(amount: 2500). This example uses integer minor units rather than binary floating-point currency and keeps the dependency's concurrency contract explicit.

I avoid creating protocols for every small type solely to satisfy a pattern. Injecting a function or an immutable value may be simpler. The important boundary is where nondeterministic or external behavior enters the feature.

Dependencies also need deliberate lifetimes. A shared account repository and a per-screen draft editor should not automatically become the same singleton. Explicit construction reveals those choices and makes previews, tests, and production configuration easier to reason about.

71. When should you use Swift Testing instead of XCTest?

I use Swift Testing for new Swift unit and integration tests when its toolchain support fits the project. It offers expressive expectations, parameterized tests, and concurrency-friendly testing APIs. Xcode 16 and later include it. XCTest remains appropriate for existing suites and for UI automation and performance testing supported by that framework.

Swift example

import Testing

@Test(arguments: [0, 1, 25])
func doubling(_ value: Int) {
    #expect(value * 2 == value + value)
}

import Testing

@Test(arguments: [0, 1, 25])
func doubling(_ value: Int) {
    #expect(value * 2 == value + value)
}

import Testing

@Test(arguments: [0, 1, 25])
func doubling(_ value: Int) {
    #expect(value * 2 == value + value)
}

This small test illustrates parameterization, not a meaningful application requirement by itself. Real tests should express behavior at a useful boundary, such as formatting a price or handling a rejected sign-in.

The two frameworks can coexist in a test target, but I do not mix their assertion and lifecycle APIs inside the same test. I also account for Swift Testing's parallel execution when tests touch shared resources; the better solution is often isolation rather than forcing the entire suite to run serially.

Choosing a test framework does not remove the need for simulator or device UI coverage, accessibility checks, and measured performance tests. Those answer different questions from a fast unit test.

72. How do you test asynchronous behavior without flaky sleeps?

I synchronize tests with observable completion or a controlled dependency instead of waiting an arbitrary number of milliseconds. If an API is async, the test can await it directly. For callback APIs, I use the testing framework's supported expectation or confirmation mechanism and an appropriate timeout as a failure bound, not as the test's scheduling strategy.

For a search feature, a fake service can hold two pending requests. The test completes the newer request first and then the older request, verifying that stale data never replaces the current result. This deliberately exercises the race instead of hoping a slow machine reproduces it.

I inject a controllable clock or sleeper for debounce and retry behavior so tests can advance logical time. I also test cancellation explicitly, including cleanup and whether cancellation is incorrectly converted into an error banner.

Actor-isolated state must be accessed through the correct isolation context in tests as well as production. Marking every test main-actor isolated can hide concurrency boundaries, so I apply it only where appropriate. Finally, I ensure child tasks and subscriptions are cleaned up before the test finishes to avoid contaminating later tests.

73. How do you support Dynamic Type without breaking the layout?

I use semantic text styles and layouts that can grow, wrap, or change arrangement as text becomes larger. In UIKit, preferred fonts or UIFontMetrics scaling must be paired with automatic content-size adjustment when the label should respond to setting changes. In SwiftUI, semantic fonts integrate naturally, but fixed frames can still clip the content.

Swift example

import UIKit

let label = UILabel()
label.font = .preferredFont(forTextStyle: .body)
label.adjustsFontForContentSizeCategory = true
label.numberOfLines = 0

import UIKit

let label = UILabel()
label.font = .preferredFont(forTextStyle: .body)
label.adjustsFontForContentSizeCategory = true
label.numberOfLines = 0

import UIKit

let label = UILabel()
label.font = .preferredFont(forTextStyle: .body)
label.adjustsFontForContentSizeCategory = true
label.numberOfLines = 0

The label still needs suitable layout constraints. For a custom font, I scale it with metrics for the intended text style instead of merely multiplying its point size by a guessed factor.

I test accessibility sizes, not just the standard range, and check buttons, error messages, tables, sheets, and navigation content. A horizontal row may need to become vertical when space is limited. Truncation is acceptable only when the product provides a meaningful way to access the full information.

Dynamic Type is not complete accessibility support by itself. Contrast, VoiceOver semantics, focus order, and nontext controls also require attention.

74. How do you make a custom control useful with VoiceOver?

I expose the control's purpose, current value, and supported action through accessibility semantics rather than expecting a screen reader to infer them from pixels. Standard controls provide much of this behavior automatically, so I prefer them when possible. A custom drawing needs deliberate labels, traits, values, and interaction support.

For a rating control, the label might be “Rating,” the value “Three of five,” and an adjustable action would change the rating. Repeating the visible label inside the value or announcing decorative images creates noise rather than clarity. Grouping should reflect a meaningful interaction unit, not simply the visual container hierarchy.

I test focus order, activation, state changes, and dismissal using VoiceOver on a device or simulator where appropriate. Accessibility Inspector can reveal missing properties, but a clean inspection report does not establish a usable spoken experience.

Dynamic updates need care: excessive announcements interrupt the user, while a critical error that appears without an accessible relationship can be missed. I preserve focus where possible and use announcements or focus changes only when they improve the interaction. Accessibility is behavior, not a checklist of labels.

75. How should an iOS app store and retrieve a secret in the Keychain?

I store small credentials in Keychain Services rather than UserDefaults or an ordinary unprotected preferences file. The item includes an appropriate class, service and account identifiers, the secret bytes, and an accessibility policy chosen for when the app legitimately needs access.

For an interactive session credential, availability only while the device is unlocked may be appropriate. Background access can require a different policy, but I choose it from the threat model and actual workflow rather than selecting the most permissive option. Device-only and synchronizable behavior also affect backup, migration, and cross-device expectations.

I inspect every returned OSStatus, distinguish a missing item from a failed read, and handle updates without creating duplicate records. I never log the secret or embed a privileged server credential in the app binary. Keychain protects stored material; it cannot make a client-held secret unknowable to a determined user controlling the client environment.

Access groups and entitlements determine sharing between authorized apps or extensions. I keep those narrowly scoped and test lock state, reinstall/account-change behavior, and failure recovery rather than assuming all Keychain operations behave like an in-memory dictionary.

76. How do you request privacy-sensitive permissions responsibly?

I request permission at the moment the user invokes a feature that needs it, explain the purpose clearly, and handle every authorization state. The required usage-description keys must match the API being used; missing configuration can cause failure rather than a useful system prompt. Permission is not a one-time Boolean because the user can change access later in Settings.

For camera capture, I first inspect authorization status. If it is undetermined, I request access; if it is denied or restricted, I provide an appropriate alternative or explanation rather than repeatedly asking. Setup and UI updates must respect the API's execution and isolation requirements.

I prefer less invasive system interfaces when they satisfy the feature. For example, a system picker may let the user select specific content without granting broad library access. That is a product design improvement, not just a way to reduce prompts.

Finally, usage descriptions, privacy manifests, and App Store privacy disclosures serve different purposes. A declaration does not grant runtime permission, and a granted permission does not authorize collecting unrelated data. I minimize collection and make the feature usable when optional permission is declined.

77. How do you choose between a background task and a background URLSession?

I choose the mechanism based on the work, not on a desire to keep the process running indefinitely. A background URL session is designed for supported file-transfer work that the system can manage outside the app's immediate foreground lifetime. BackgroundTasks schedules app work under system control, with no promise that it will start at an exact requested time.

A short background execution assertion can help finish an already-started foreground operation during a transition, but it is time-limited and must be ended correctly. It is not a replacement for a durable job scheduler. For scheduled tasks, I register handlers correctly, submit appropriate requests, handle expiration, and report completion.

The work should be resumable and idempotent because execution can stop. I persist enough progress to recover and avoid doing large CPU or network work merely because a timer fired before the app went to the background.

I test foreground-to-background transitions, expiration, cancellation, and relaunch recovery. Simulator experiments alone do not establish real scheduling behavior under battery and system policy. Background execution eligibility, capabilities, and transfer constraints must be checked for the selected API and deployment target.

78. How do notification delivery and notification handling differ?

Delivery is the system getting a local or remote notification to the device; handling is the app deciding what to do when it arrives or the user interacts with it. These paths differ for foreground presentation, a tap that opens the app, an action button, and a background update. I route each path into a small, explicit application action rather than assuming every notification launches the same screen immediately.

A push payload is untrusted input. I validate identifiers, avoid embedding sensitive content unnecessarily, and fetch authoritative data after authentication. A notification may be delayed, duplicated, outdated, or never delivered. Critical business state cannot depend on receiving exactly one push.

For foreground notifications, the notification-center delegate controls the appropriate presentation behavior. For user actions, I complete the callback after the required handling and avoid leaving completion handlers hanging. Scene readiness and login state may require deferring navigation.

I test cold launch, an already-running app, denied notification permission, expired content, and account changes. Background pushes and visible alerts have different capabilities and delivery policies; neither should be described as a guaranteed general-purpose background execution mechanism.

79. How do you validate and route a universal link?

I treat a universal link as an external request to navigate, not as permission to perform an action. The website association and app entitlement establish which HTTPS links may be delivered to the app, but the app must still validate the host, path, parameters, and requested operation.

I parse with URL components, map the result to a small route type, and reject unsupported or malformed inputs. A route such as an order identifier should resolve through normal authentication and authorization checks. It should not bypass access control because it arrived through a trusted-looking domain.

Routing also depends on application state. During a cold launch I may need to retain the pending route until the correct scene and account are ready. If the referenced item was deleted or belongs to another account, I show a useful fallback rather than crashing or exposing stale cached data.

Tests cover percent encoding, unexpected query parameters, duplicate parameters, missing IDs, logged-out users, multiple scenes, and web fallback. Association-file configuration and caching can complicate debugging, so I separate delivery failures from parser and navigation failures instead of changing both layers at once.

80. How do you manage Combine subscriptions and UI updates safely?

I keep each subscription's lifetime aligned with the feature that needs it and choose explicitly where downstream UI work executes. AnyCancellable cancels when released, so storing cancellables in an owner keeps the pipeline active. That owner can also cancel or replace work when the feature ends or its inputs change.

A subscription closure can strongly capture its owner, producing a cycle when the owner retains the cancellable. I use an appropriate capture strategy or explicit teardown, not a blanket rule that every closure must be weak. A finite operation and an infinite notification stream have different retention risks.

receive(on:) affects downstream scheduling, while subscribe(on:) affects upstream subscription-related work; they are not interchangeable. Receiving values on the main dispatch queue is also not a complete substitute for expressing Swift actor isolation in strongly checked code. I keep model isolation clear at the boundary.

I test initial values, errors, cancellation, and repeated subscriptions. When bridging a publisher into an async sequence, I check buffering and demand behavior rather than assuming every event is preserved automatically. The choice between Combine and async sequences should follow the existing API and stream semantics.

iOS Interview Questions and Answers for Experienced

The experienced answers examine trade-offs in large, long-lived applications. They prioritize explicit guarantees, measured evidence, recoverable failures, and safe incremental change rather than prescribing one architecture for every team.

81. How would you choose an architecture for a growing iOS application?

I would choose boundaries around product capabilities and ownership, then select the simplest presentation pattern that makes those boundaries testable. MVC, MVVM, and unidirectional state management are tools, not substitutes for deciding who owns navigation, persistence, authentication, and asynchronous work. A small feature may need only a view and a model; a checkout flow usually needs explicit state transitions and service boundaries.

For example, a checkout feature could depend on an ordering interface rather than a concrete network client. Its presentation state distinguishes editing, submitting, confirmation, and failure. The server remains authoritative for price and order acceptance; the UI cannot manufacture a successful purchase by changing local state.

I would review dependency direction, cancellation ownership, error mapping, and test seams before adding abstraction. I would also identify the expected change: another payment provider, an offline mode, or a second UI. A good architecture makes those changes local while keeping ordinary development understandable; maximizing the number of protocols and layers is not the goal.

82. How would you modularize an iOS codebase without creating excessive build complexity?

I would extract modules along stable feature or infrastructure boundaries, starting with a dependency graph rather than moving every folder into its own package. Features should not import each other’s internal models indiscriminately. A composition layer can provide services and connect navigation while shared domain types remain small and intentional.

I would first measure clean and incremental build times, identify a relatively independent feature, and move its public surface behind a narrow interface. Resources, localization, test fixtures, and dependency versions must move deliberately too. A module that exposes all its internals through public declarations provides little isolation and makes future changes harder.

The trade-off is real: many tiny modules increase configuration and integration costs, while a single shared module can become a dependency bottleneck. I would enforce acyclic dependencies and ownership in review, add contract tests around important boundaries, and compare build and onboarding outcomes after extraction. Swift packages can organize reusable code, but package adoption alone does not guarantee faster builds or a better architecture.

83. How would you design navigation that supports deep links and multiple presentation styles?

I would represent destinations as application data and keep route interpretation separate from view construction. A route might identify an order by its stable identifier, rather than retain a particular controller or an entire mutable order object. The navigation owner then translates that route into a push, sheet, or split-view selection appropriate to the current scene.

Deep links, notification taps, and ordinary buttons should enter the same validation and authorization path. If login is required, I would retain a validated pending destination and resume it after authentication, rechecking access rather than trusting the original URL. Missing or deleted content needs a useful fallback.

I would test navigation as state transitions: repeated links, links arriving during an existing presentation, cancellation of login, and restoration into changed data. SwiftUI’s NavigationStack is available from iOS 16, but the routing design need not depend on it; UIKit coordinators can apply the same separation. Avoid a global navigation singleton that accidentally directs one window’s action into another window.

84. How would you separate shared application state from per-scene state?

I would share durable domain data and account services where appropriate, but give each scene its own navigation, selection, presentation, and transient editing state. Two windows can display the same document without being forced onto the same screen. A process-wide account change may invalidate both, whereas a selection change in one window usually should not.

For a document app, I would distinguish the document repository from each scene’s editor session. Shared document updates need a deliberate consistency policy; an unsaved local edit cannot simply be overwritten because another window refreshed its data. Scene identity should also be carried through commands that present alerts or open destinations.

I would test two active windows, one background window, scene disconnection, and account switching. A scene going inactive does not mean the whole application has stopped, and scene disconnection is not a reliable final-save opportunity. Persist meaningful changes as they occur and reserve lifecycle callbacks for bounded coordination. This avoids global-window assumptions and makes multiwindow bugs reproducible.

85. How would you make state restoration resilient to application and data changes?

I would restore a compact, versioned description of the user’s context rather than serialize an entire live view hierarchy or service graph. Useful restoration data includes a document identifier, selected tab, navigation identifiers, and perhaps a draft reference. Credentials and authoritative business data belong in their appropriate stores, not in a navigation snapshot.

Restoration is a best-effort input. I would validate the schema version, confirm the account and permissions, reload current records, and fall back gracefully when a destination no longer exists. A restored payment screen must not automatically repeat a purchase. Large drafts should be saved independently so losing UI restoration does not lose user work.

For scene-based UIKit apps, NSUserActivity can carry restoration information for a scene. SwiftUI can preserve suitable scene-scoped values, but the application still owns compatibility and validation. Tests should cover an older saved route after an upgrade, removed content, logout, and an interrupted edit. I would not depend on a termination callback to create the only persistent copy.

86. How would you migrate a UIKit application to SwiftUI incrementally?

I would migrate a bounded screen or reusable component first, keeping stable application services and persistence outside the UI technology boundary. UIHostingController can host SwiftUI in UIKit, and representable wrappers can expose UIKit components to SwiftUI. That allows gradual adoption without rewriting authentication, networking, and every navigation path at once.

The main risks are ownership and lifecycle mismatches. I would define which layer owns the model, which owns navigation, and how updates flow back. A wrapper should update an existing UIKit view rather than recreate expensive state on every SwiftUI update. Delegates, observers, and task lifetimes must be cleaned up when the bridged component is no longer needed.

I would compare accessibility, keyboard behavior, restoration, performance, and minimum-OS support before expanding the migration. Snapshot similarity alone is insufficient: a visually identical screen can lose focus, duplicate requests, or change navigation behavior. I would retain an integration test around the mixed stack and migrate because the new implementation improves maintainability or capability, not merely to eliminate UIKit.

87. How would you migrate a large codebase to Swift 6 concurrency checking?

I would separate the compiler upgrade from enabling Swift 6 language mode, then migrate target by target. First I would enable complete concurrency diagnostics in the existing language mode, inventory shared mutable state and callback boundaries, and agree on ownership rules. UI-facing state often belongs to MainActor; other state may belong to an actor or an immutable value model.

I would fix the underlying design before suppressing diagnostics. For example, sending an immutable request value across a boundary is usually clearer than passing a mutable controller. An unchecked Sendable conformance is a promise that requires an audited synchronization invariant, not a convenient way to silence the compiler.

Each migrated target needs tests around callback queues, cancellation, and integration with older modules. I would track temporary escape hatches with an owner and removal condition. Language mode, default actor isolation, and enabled upcoming features should be recorded in the build configuration; two projects using the same compiler can have different concurrency behavior because their settings differ.

88. How would you decide where CPU-intensive work should execute?

I would identify the operation’s isolation and measure its cost before deciding how to move it. Marking a function async does not make expensive synchronous work run on a background thread. A task created inside an actor-isolated context can inherit that isolation, so placing image processing inside Task from a UI model may still block UI work.

I would move suitable computation behind an explicitly designed non-UI execution boundary, pass safe input values, and return a result for the UI owner to apply. The exact annotations depend on the Swift language features and isolation settings enabled by the project. I would verify those settings rather than assume all nonisolated async functions behave identically across toolchains.

The work also needs bounded parallelism, cancellation checks, and memory limits. Spawning a detached task per image can overwhelm a device even if no data race occurs. I would profile the result on representative hardware and confirm both interaction latency and total resource cost, not just that the code uses async syntax.

89. How would you protect business invariants across actor suspension points?

I would assume actor state may change whenever an actor method suspends. Actor isolation prevents simultaneous unsynchronized access to its protected state, but it does not make an entire async operation transactional. A read, await, and write sequence can therefore produce a logical race even when the compiler reports no data race.

For a reservation service, I could record a pending reservation before making the remote call, then reconcile success or failure afterward. Alternatively, I could attach a version to the state read before suspension and verify that version before committing the result. The right design depends on whether duplicate work, stale responses, or temporary reservation failures are acceptable.

I would keep synchronous invariant-preserving transitions small and explicit. Tests should deliberately suspend the external operation, issue another actor call, then resume the first operation in a different order. I would not block an actor with a semaphore to simulate a transaction; that can undermine forward progress and does not solve distributed consistency with a server.

90. How would you coordinate authentication refresh when many requests expire together?

I would centralize refresh coordination and let concurrent requests share one in-flight refresh operation. Each request should know which credential generation it used. If a response arrives after another request already refreshed credentials, it may be sufficient to retry with the newer generation instead of starting another refresh.

The coordinator needs more than a cached Task. I would define how logout invalidates an in-flight result, how refresh failures are shared, and whether cancellation of one waiter should cancel work needed by others. A session identifier or generation prevents a late refresh from restoring credentials after the user has signed out or switched accounts.

Retries must be bounded. A second authentication failure should return a controlled error rather than create a refresh loop. Non-idempotent requests also need a server-supported replay policy before being automatically repeated. I would test simultaneous failures, token rotation, logout during refresh, and refresh completion after account switching, without exposing tokens in logs or test failure messages.

91. How would you design offline editing and conflict resolution?

I would store edits locally before presenting them as durable, then synchronize through an explicit pending-operation queue. Each operation needs a stable identifier, target record, base version, and a state such as pending, acknowledged, or conflicted. The UI should distinguish saved on this device from confirmed by the service.

Conflict policy must follow the data’s meaning. Merging independent fields may be reasonable for a profile, while inventory quantities or financial records often require server-side validation. Blind last-writer-wins based on device clocks can discard work and is unsafe when clocks disagree. Deletions need tombstones or another deliberate mechanism so old clients do not resurrect records.

I would test airplane-mode edits, process termination, repeated delivery, account switching, and two devices editing the same item. Core Data can persist local state, but choosing it does not automatically define a custom server synchronization protocol. The design should expose conflicts to users when automatic resolution would invent an answer or destroy meaningful changes.

92. How would you prevent duplicate server-side operations after retries or app restarts?

I would give the logical operation a stable identifier before its first submission and use a server-supported idempotency contract. The identifier must survive a process restart and remain the same when retrying that operation. Generating a new identifier on each retry defeats deduplication.

For an order, I would persist the pending request and its identifier together, send it, and reconcile an ambiguous timeout by querying or retrying according to the server contract. The server must define the scope, retention period, and response for reusing an identifier with a different payload. Client-side button disabling helps user experience but cannot provide distributed exactly-once execution.

I would test a successful server commit followed by a lost response, a crash before local acknowledgement, and simultaneous retries. Sensitive payloads should not be persisted unnecessarily. If the API has no idempotency support, I would document that limitation and design an explicit status-check or user-confirmation flow instead of claiming that URLSession retry logic guarantees one business action.

93. How would you make large file transfers survive suspension?

I would use a background URLSession configuration for supported upload or download transfers and persist a mapping between transfer identifiers and business records. The system manages those transfers separately from ordinary foreground execution, so the app must reconstruct the session and reconcile its tasks after relaunch rather than assume its in-memory objects survived.

The delegate-driven completion path needs careful ownership. A downloaded temporary file must be moved to durable storage before it disappears, and completion of background session events must be reported through the appropriate lifecycle integration after processing finishes. Background uploads should use file-backed upload tasks rather than assume an in-memory body will survive termination.

I would test suspension, system termination, network loss, duplicate callbacks in application-level reconciliation, and user cancellation. A user force-quit changes the behavior: it is not valid to promise that the system will continue and relaunch the app as if nothing happened. Progress displayed after reopening should come from reconciled task state, not a stale percentage saved by the previous process.

94. How would you choose an iOS background execution strategy?

I would choose the mechanism according to the work, rather than treat background execution as a general-purpose daemon. Background URLSession fits supported file transfers. BGAppRefreshTask can refresh modest amounts of content, while BGProcessingTask is intended for longer processing opportunities under system scheduling. A short background assertion can help finish an already-started operation, but it is not unlimited runtime.

I would make the work resumable, persist progress, and stop promptly when its expiration handler runs. Scheduling is a request, not a promise of an exact execution time. Earliest-start constraints and eligibility do not create a reliable alarm clock, and users’ device conditions and settings affect opportunities.

The product must still work when background work is delayed or never runs before the next foreground launch. I would perform a bounded reconciliation then, avoid duplicate processing, and report freshness honestly. Tests should exercise expiration and interrupted checkpoints in addition to the happy path. Newer specialized background APIs require their own availability and entitlement checks before adoption.

95. How would you design push notification handling so it remains correct when delivery is unreliable?

I would treat a push as a signal to inspect authoritative state, not as the only copy of a business event. Notifications can be delayed, duplicated, coalesced, or unavailable under some conditions. A silent notification is not a guaranteed background job, so the app should reconcile important state when it becomes active.

The payload should carry minimal identifiers rather than secrets or complete sensitive records. When the user taps it, the app validates the destination, checks the current account, fetches the latest data, and handles removed or inaccessible content. Notification authorization, device registration, and the server’s account-to-device association are separate concerns.

I would make handling idempotent and update registration when the system provides a new token. Logout should remove the relevant server association without relying on token secrecy alone. Test cases include a tap during cold launch, an expired login, multiple windows, permission denial, and a stale notification after account switching. Delivery analytics should distinguish provider acceptance from actual user display or action.

96. How would you secure deep links and diagnose universal-link failures?

I would parse links into an allowlisted route model and treat every parameter as untrusted input. A recognized domain does not authorize the current user to view a record or perform a destructive action. I would reject unexpected schemes, hosts, path shapes, oversized values, and unsupported identifiers, then enforce authorization when loading the destination.

Universal links also require a correct association between the app entitlement and the website’s apple-app-site-association file. I would inspect those separately from in-app routing. Testing only by typing a URL into Safari’s address bar is misleading because that is browser navigation, not a normal universal-link activation test.

I would test a link from another app, a cold and warm launch, an uninstalled app, and user-selected browser preferences. Failures may involve cached association data or the context in which the link was opened, not just a broken route handler. Login callbacks need their own validated state and redirect rules; ordinary content routing should not silently become an authentication bypass.

97. How would you design secure credential storage and logout behavior?

I would keep long-lived credentials in Keychain with the narrowest accessibility and sharing settings that satisfy the product. UserDefaults and ordinary configuration files are not credential vaults. The accessibility class must match the actual need: a secret required only while the user is interacting should not automatically be made available to background work.

Logout is a state transition across several components. I would invalidate the session generation, cancel or quarantine account-scoped work, clear relevant cached data, remove locally stored credentials, and revoke server credentials when the protocol supports it. A late network response must not repopulate the previous user’s state.

I would test device locking, authentication failures, access-group configuration, account switching, and reinstall-related assumptions instead of treating Keychain as a generic dictionary. Biometric access control can protect local use of an item, but it does not replace server authorization. Logs, crash breadcrumbs, screenshots, and clipboard behavior also need review because secure storage cannot prevent secrets from leaking through unrelated application paths.

98. How would you evaluate ATS exceptions and certificate pinning?

I would keep App Transport Security’s secure defaults and fix the server configuration before considering an exception. If an exception is unavoidable, I would make it narrowly scoped, document the business reason, and set a removal plan. A broad exception to make development easier can accidentally weaken production traffic.

Certificate pinning is an additional trust restriction, not a substitute for ordinary certificate validation. It introduces operational risk: certificate rotation or a missing backup strategy can disconnect every installed client. Before using it, I would define the threat model, supported endpoints, pin lifecycle, and recovery path. I would never implement a challenge handler that accepts every certificate after pin validation fails.

The test plan should include expired certificates, an untrusted chain, hostname mismatch, legitimate rotation, and captive or intercepted networks. Sensitive authorization decisions remain on the server even with TLS and pinning. I would also distinguish URL Loading System protections from custom networking stacks rather than assume one plist setting audits every byte the application sends.

99. How would you keep privacy declarations aligned with application behavior?

I would maintain a data inventory covering both first-party code and third-party SDKs, then compare actual behavior with privacy manifests, permission prompts, and App Store privacy disclosures. These mechanisms serve different purposes; adding a manifest does not grant permission or make a data practice acceptable.

For required-reason APIs, the declared reason must match the real use. I would inspect transitive dependencies and archive outputs instead of checking only source files in the main target. If a feature collects data for a new purpose, that change should trigger a review of retention, sharing, consent, and user-facing disclosure before release.

I would build this into dependency updates and release review rather than handle it as a one-time submission task. Tests can exercise permission denial and restricted access, while code review can catch unnecessary identifiers or logging. Apple’s covered API lists and submission rules change, so the release owner should verify current requirements. A successful upload is not proof that the app’s privacy behavior matches its claims.

100. How would you assess a third-party iOS SDK before adopting or upgrading it?

I would evaluate the SDK’s necessity, public API stability, maintenance history, license, privacy behavior, and integration cost before measuring how quickly its sample app runs. I would inspect transitive dependencies, binary provenance where available, resource packaging, minimum deployment requirements, and any required capabilities or privacy declarations.

The evaluation should use a small isolated integration with realistic startup, memory, networking, and failure measurements. For example, an analytics SDK that initializes synchronously at launch may have a larger product cost than its package size suggests. I would also verify how it behaves without permission, offline, during logout, and when its backend is unavailable.

I would wrap only the application-specific boundary that needs insulation, not blindly mirror every SDK method. Version updates should be pinned and reviewed with reproducible build evidence and a removal strategy. An SDK signature or approved privacy manifest is useful evidence, but neither proves that the dependency is secure, necessary, or compliant with the application’s own promises.

101. How would you plan a persistent-store migration without risking user data?

I would treat migration as a release feature with fixtures from every supported stored schema, not merely verify that a new installation works. I would define the mapping, data invariants, expected disk requirements, and recovery behavior before changing the model. Lightweight migration is appropriate only when the changes fit its supported transformations.

For a complex change, I would use an explicit staged or custom migration strategy and test realistic large stores, interrupted execution, low disk space, and invalid legacy values. The result should preserve identifiers, relationships, and user-created content. Deleting the store on an opening error is not an acceptable default for irreplaceable data.

The release plan must consider older application versions and server compatibility too. A newly migrated store may not be readable by an older binary, so a rollback story cannot simply assume downgrade is harmless. I would retain migration diagnostics without recording private records, and distinguish a recoverable migration problem from corruption that needs a separate investigation and user-safe recovery path.

102. How would you investigate a regression in application launch time?

I would first define the launch being measured: cold or warm, restored or clean, signed-in or signed-out, and device class. I would compare equivalent release builds and separate time to the first usable interface from optional content loading. A network request that finishes later should not necessarily block interaction.

I would inspect initialization work, synchronous disk access, dependency setup, database opening, and expensive view construction on the launch path. Third-party SDK initialization and eagerly creating services can be hidden contributors. I would move nonessential work later only if doing so does not simply transfer a large stall to the first tap.

The fix should have a measurable hypothesis, such as deferring an unused image index or reducing the records loaded for the initial screen. I would add a regression test and inspect distribution metrics across devices after release. A simulator or debugger-attached improvement is useful during development but does not by itself establish a real-device production improvement.

103. How would you diagnose an intermittent UI hang?

I would capture what the main execution path is doing during the hang, then distinguish expensive computation from blocking and contention. A busy stack suggests work such as decoding or layout; a waiting stack may point to a lock, synchronous I/O, or a dependency cycle. Simply dispatching the same operation elsewhere can hide the symptom while preserving the underlying contention.

I would reproduce the interaction with representative data and inspect a time profile or hang diagnostic around that interval. Useful context includes navigation state, request timing, database operations, and whether another task owns a resource the UI needs. I would not log sensitive payloads just to obtain that context.

The fix should reduce or remove the dependency that delays interaction: batch work, avoid synchronous waits, shorten critical sections, or isolate computation appropriately. I would verify that cancellation and error paths remain correct. Finally, I would test the same interaction under load and on slower supported hardware, because a fast development device can conceal a user-visible stall.

104. How would you improve scrolling performance in a media-heavy feed?

I would profile the feed before choosing an optimization and separate data loading, image decoding, layout, and rendering costs. Stable identifiers and correct reuse prevent unnecessary work, but they do not make oversized images cheap. I would load appropriately sized assets, limit concurrent requests, cache within a budget, and cancel or deprioritize work for items no longer needed.

The cell or row should represent a stable item identity so an old image response cannot appear in a reused row. Expensive formatting and decoding should not be repeated in every presentation update. Prefetching can help, but an unbounded prefetch queue increases memory and network pressure and may worsen responsiveness.

I would test long feeds, fast direction changes, poor connectivity, Dynamic Type, and mixed media dimensions. Improvements should be judged by interaction consistency and resource use, not only average frame rate. I would also verify that virtualization or custom drawing has not removed accessibility information, broken focus behavior, or made content impossible to inspect with assistive technology.

105. How would you distinguish a memory leak from excessive but temporary memory use?

I would observe memory across repeated instances of the same workflow and inspect which objects remain reachable afterward. A retain cycle can keep a controller and its model alive indefinitely, but a large image cache or a batch import may legitimately retain memory under an overly generous policy. Both can hurt users, but their fixes differ.

I would use allocation histories and a memory graph to follow ownership paths, checking closures, delegates, observers, timers, and long-lived tasks. Weak references are not a universal cure; removing a necessary owner can make work disappear prematurely. The goal is to match lifetime to responsibility and bound caches or batches deliberately.

I would repeat navigation and account changes, trigger cache eviction, and test with realistic assets. I would also inspect memory-pressure terminations separately from ordinary crash stacks. A falling object count is encouraging, but peak memory can still exceed what a device can tolerate. There is no single universal safe memory limit that applies to every iPhone and system condition.

106. How would you reduce battery usage without compromising required behavior?

I would identify which user workflows consume energy and examine repeated wakeups, networking, location use, CPU work, and unnecessary rendering. The first question is often whether the work needs to happen at all. Polling every few seconds or requesting precise continuous location can dominate energy even when each callback is short.

I would batch compatible requests, stop observers and timers when their results are not needed, use an appropriate location accuracy and update policy, and make caches avoid redundant work. Deferring work is useful only when it respects the feature’s freshness and reliability requirements. A background mode should not be enabled merely to keep code running.

Measurements should compare equivalent device conditions, not a charging simulator with a real phone. I would test low-power behavior, prolonged background periods, and a representative full session. The acceptance criteria should include data freshness and user experience alongside energy reduction, because silently failing to update important content is not a successful optimization.

107. How would you investigate a crash reported only in production?

I would identify the exact build, operating-system version, device context, and termination category, then obtain a complete report and matching symbols. The top application frame is a clue, not automatically the root cause; memory corruption or an earlier invariant violation can surface elsewhere. I would distinguish an exception or signal from a watchdog or memory-pressure termination.

I would group related reports by meaningful stack and context, inspect recent changes, and build a minimal reproduction with the same data shape or timing. Matching archive and symbol files are essential; symbols from a similar-looking build can mislead the investigation. If telemetry is incomplete, I would add narrowly scoped, privacy-safe diagnostics rather than log entire responses.

A fix needs a regression test or a reproducible explanation of the failure mechanism, followed by monitoring of the affected cohort. I would not close the incident solely because the crash disappears on the latest developer device. Sampling, delayed reports, and users staying on older versions all affect what production evidence means.

108. How would you design privacy-conscious production observability for an iOS app?

I would collect the smallest set of signals needed to answer operational questions: which release regressed, which workflow failed, and how often users were affected. Events should use bounded categories and operation identifiers rather than raw names, URLs with tokens, document contents, or complete request bodies. Retention and access control belong in the design from the start.

I would combine application-level events with platform diagnostics such as MetricKit and Xcode Organizer data, recognizing that collection and delivery are not a complete real-time record of every session. The supported MetricKit integration must match the deployment target; newer APIs should not be assumed available to every installed client.

I would define success and failure events together so a missing completion can be investigated without creating a false error count. Sampling and consent states should be visible in dashboards. Tests should verify redaction and bounded payload size. Observability is most useful when it can explain a specific release decision while revealing as little personal data as possible.

109. How would you choose the right balance of unit, integration, and UI tests?

I would place tests at the lowest layer that can detect the intended failure accurately, then add a small set of end-to-end journeys for integration risk. Pure pricing or validation logic belongs in fast unit tests. Repository tests should cover serialization and persistence boundaries, while UI tests verify behavior such as login, navigation, accessibility, and recovery from a failed submission.

A high unit-test count does not compensate for never testing the actual network mapping or persistent schema. Conversely, putting every edge case into UI automation makes the suite slow and difficult to diagnose. I would prioritize irreversible actions, offline transitions, account isolation, and data migration based on product risk.

Swift Testing can be used for modern unit tests with an appropriate toolchain; XCTest remains relevant, particularly for UI testing and established performance infrastructure. I would avoid tests that depend on execution order or a developer’s account. Every test should provide a clear failure signal and own the data and resources it creates.

110. How would you make asynchronous tests deterministic?

I would control the dependencies that introduce timing instead of adding arbitrary sleeps. A fake service can suspend until the test explicitly supplies a result, and an injected clock or scheduling abstraction can make retries and debounce behavior advance predictably. Tests should assert observable state transitions rather than hope that a task finishes within a guessed delay.

For a search model, I would start query A, then query B, complete B first, and complete A last. The assertion is that A cannot overwrite B’s displayed result. A separate test would cancel the operation and verify the cancellation policy. This directly exercises the race rather than relying on a fast local network to avoid it.

I would also ensure test-owned tasks are awaited or cancelled before the test exits. Shared singletons and unstructured background tasks can leak state into later tests, especially when the runner executes tests concurrently. Timeouts remain useful as failure bounds, but they should not be the mechanism that makes the behavior correct.

111. How would you make accessibility part of release quality rather than a final checklist?

I would include accessibility in component contracts, design review, and acceptance tests. Controls need meaningful labels, traits, values, and actions; a custom interface should preserve a sensible reading and focus order. Dynamic Type, contrast, reduced motion, and alternative input need to be considered while layout and interactions are still easy to change.

I would run automated audits on important screens and manually complete critical workflows with VoiceOver and relevant system settings. Automated audits can catch issues such as missing descriptions or clipped text, but passing them does not prove that a person can understand a complex workflow. Error messages, loading changes, and modal transitions deserve special attention.

For a checkout flow, success means a user can identify the total, edit details, understand validation, and confirm the order without relying on visual position or color alone. I would add regression coverage for those semantics and test large text with localization. Accessibility defects should have owners and release impact like other functional defects, not be treated as cosmetic polish.

112. How would you prepare an iOS application for localization and regional differences?

I would separate user-facing text and formatting from business identifiers, use localizable resources, and provide translators with context. String catalogs can manage translations and plural variations, but concatenating fragments into sentences still produces poor results. Dates, numbers, currency, and units should use locale-aware formatting rather than manually assembled punctuation.

I would distinguish display formatting from business rules. A currency code or transaction timestamp should not change meaning because the device’s region changes. Server interchange formats need explicit, stable contracts, while the interface can adapt its presentation to the user. Calendar, time-zone, and daylight-saving cases need tests when the product depends on dates.

The UI should handle longer translations, right-to-left layouts, and accessibility text sizes together. I would use pseudolocalization early and test real supported languages before release, including image assets and permission descriptions. A screen that fits English on one phone is not evidence that its layout or wording will work worldwide.

113. How would you design a reliable iOS CI and signing pipeline?

I would make builds reproducible by fixing the intended Xcode toolchain, dependency resolution, configuration, and source revision, then separating test execution from release authorization. The pipeline should archive the exact candidate intended for distribution and retain its symbols and relevant build metadata. A green simulator test is not equivalent to a successfully signed distributable archive.

Signing identities, provisioning profiles, entitlements, and bundle identifiers must agree. Credentials belong in a controlled secret store with least privilege, not in the repository or verbose build logs. I would validate capabilities for each target, including extensions, and verify the archive’s actual configuration rather than trust a scheme name.

The test matrix should cover supported OS versions and representative device behavior according to risk. I would fail clearly on missing signing assets or unexpected toolchain changes instead of silently switching settings. Production upload and release remain explicit controlled stages, with traceability from commit to archive to distributed build and a documented recovery process.

114. How would you use TestFlight and phased release to reduce deployment risk?

I would use TestFlight to validate a release candidate with representative users and scenarios, then define measurable release gates before exposing the update more broadly. The beta should exercise migrations, permissions, account transitions, and real device conditions, not just show that the app launches. Feedback should be tied to the exact build.

A phased App Store update can reduce initial exposure through automatic updates, but it is not a security boundary or an exclusive allowlist. Users can manually download the update during the phase. I would monitor crash rates, critical workflow completion, and data integrity by version, with a named owner who can pause expansion.

Pausing a phase does not remove the binary from devices that already installed it. Therefore, server compatibility and safe feature controls matter before release. I would prepare a forward-fix path and verify that older supported clients continue to function. A rollout succeeds when evidence supports expansion, not simply because a calendar interval has elapsed.

115. How would you respond to a severe defect discovered after an App Store release?

I would first limit harm while preserving evidence: pause a phased rollout if applicable, disable the affected feature through an already-designed safe control when possible, and protect server-side invariants. I would not assume an installed iOS binary can be instantly rolled back. Users may remain on the defective version until they update.

The response depends on the failure. A visual issue may tolerate a normal fix, while data corruption requires stopping the damaging path and planning reconciliation. I would identify affected versions and cohorts, keep backend behavior compatible with existing clients, and prepare a tested replacement build. Feature controls must themselves fail safely when configuration is unavailable.

I would communicate the user impact and recovery steps without overstating certainty. After mitigation, I would verify that new incidents decline and that already-affected data is addressed. The retrospective should produce specific prevention work, such as a migration fixture, a release gate, or safer protocol semantics, rather than merely promise more testing next time.

116. How would you choose a deployment target and adopt newer platform APIs safely?

I would choose the minimum supported OS from product needs, user impact, maintenance cost, and required platform capabilities, not from the newest SDK number alone. The compiler version, Swift language mode, SDK used to build, and minimum deployment target are separate decisions. Building with a newer SDK does not automatically make every API available on older devices.

For a newer feature, I would use availability annotations and runtime availability checks where appropriate, then provide a real fallback or make an intentional product-level support change. The fallback must be tested; an empty branch that compiles is not a satisfactory experience. Toolchain-only features and runtime framework APIs require different compatibility reasoning.

I would maintain a small documented support matrix and review it during upgrades. Store submission requirements can change independently of the app’s minimum OS, so they should be checked at release time. Adoption should be driven by a concrete benefit such as improved capability or reduced complexity, with measured impact on the users who would be excluded.

117. How would you maintain a safe boundary between Swift and Objective-C code?

I would expose a small, explicit interoperability surface and document nullability, ownership, error conventions, and callback execution context. Objective-C callers do not automatically gain every guarantee expressed by Swift’s type system or concurrency model. A declaration that looks convenient in Swift can still be ambiguous when imported across the boundary.

For an older callback-based service, I would verify whether completion is called exactly once, on which queue, and whether cancellation is supported before creating an async wrapper. A checked continuation detects some misuse, but it cannot invent missing completion behavior. I would also distinguish Objective-C exceptions from Swift Error handling; a normal do-catch is not a general exception recovery mechanism.

Migration tests should exercise nil values, failed operations, deallocation, and callbacks after cancellation. I would avoid broad unchecked concurrency annotations around an entire legacy subsystem. Instead, I would constrain access at a well-defined boundary and pass immutable values where practical, making the remaining assumptions visible to reviewers and future maintainers.

118. How would you share data safely between an app and its extensions?

I would treat an extension as a separate process with its own lifecycle and resource limits, not as another screen inside the containing app. An App Group can provide an authorized shared container, but it does not automatically coordinate simultaneous reads and writes or make every application API valid in an extension.

I would define a small shared data contract and choose storage with an appropriate consistency mechanism. For example, a widget can read a compact, versioned snapshot rather than open the app’s entire object graph. If both processes modify a database, the persistence design must support that access and propagate changes deliberately; an in-process actor protects only its own process.

The test plan should include older snapshots, partial updates, account logout, and the app not running. Shared secrets need appropriate Keychain access groups rather than being copied into an ordinary shared file. I would also check extension-safe APIs and budget expensive work carefully, because code that behaves acceptably in the main app can exceed an extension’s constraints.

119. How would you design caches that remain correct across accounts and changing data?

I would define the cache key, freshness policy, ownership, and invalidation triggers before selecting a cache implementation. The key may need account identity, resource identifier, query parameters, locale, or an image size variant. Omitting one can return another user’s content or a result with the wrong representation.

I would distinguish transport caching from application caching. HTTP cache directives and validators can help with network responses, while an image cache or offline database may have separate rules. Sensitive account-scoped entries should be cleared or made inaccessible on logout, and a stale response from an old session must not populate the new session’s cache.

The design also needs bounds and a miss path. Memory pressure can remove cached data, disk data can become invalid, and offline users may need a clearly labeled stale result rather than a misleading success. I would test concurrent refreshes, eviction, account switching, and server invalidation. A cache should improve performance without becoming an undocumented second authority for business data.

120. How would you lead a technical review of an unfamiliar iOS application?

I would begin with a critical user journey and trace it through navigation, state ownership, networking, persistence, and failure recovery. That reveals more than reviewing every file alphabetically. I would ask how the app behaves when the network fails, the process terminates, the user changes accounts, or a stored schema is upgraded.

Next I would inspect build configuration, deployment support, signing boundaries, dependency inventory, privacy declarations, and the test and release process. I would profile one realistic workflow rather than infer performance from code style. Findings should distinguish demonstrated defects, plausible risks, and maintainability preferences so the team can prioritize responsibly.

The output should be a short ranked plan with evidence, user impact, an owner, and a verification method for each item. I would preserve working behavior and recommend incremental changes unless the evidence justifies a larger redesign. A successful review helps the team ship safer improvements; it does not reward replacing every unfamiliar pattern with the reviewer’s favorite architecture.

iOS Documentation and Further Reading

Examples distinguish Swift language and compiler settings from iOS SDK availability. Observation and SwiftData require iOS 17 or later; NavigationStack requires iOS 16 or later. Concurrency behavior depends on language mode and enabled isolation features. Check the installed Xcode SDK, deployment target, current scene-lifecycle requirements, and App Store submission guidance before production use.

Use these official Apple and Swift resources to verify the language, frameworks, and delivery practices discussed in this guide. This is a curated reading list, not every source consulted. Documentation review and local static checks do not establish successful compilation, device behavior, production security, or App Store acceptance.

Team Sherlock.sh

Building fraud intelligence for live high-stake video interactions (financial discussions, hiring interviews, board meeting etc).

Catch candidate fraud before it costs you.

Sherlock AI helps you detect AI-assisted, proxy, and deepfake candidates, so every hiring decision is backed by evidence.

Ask AI about Sherlock.sh

Similar Posts