iOS interview preparation

iOS Interview Questions

20 frequently asked iOS interview questions. iOS development is an in-demand field focused on building Swift, SwiftUI, and UIKit apps, solving lifecycle, concurrency, networking, storage, performance, testing, and App Store release tasks. The questions cover different levels, and you can practice answering them aloud in our interview trainer.

Start an iOS AI InterviewNo credit card required. 1 free session available.
Technical interview practice in EnglishA mode where non-native speakers can practice passing technical interviews.

Beginner questions

1What responsibilities should a UIViewController have in a typical iOS MVC architecture?

In classic iOS MVC (Model-View-Controller), UIViewController acts as the Controller mediating between View components and Model data. Its primary responsibilities include managing the view lifecycle (e.g., viewDidLoad, viewWillAppear), setting up and updating UI elements, handling direct user interactions (such as button taps, delegates, and target-actions), and orchestrating screen transitions or presentations. Non-UI responsibilities—such as raw networking, persistence, and heavy business logic—should be delegated to dedicated service or model objects to prevent the view controller from becoming a Massive View Controller.

class UserProfileViewController: UIViewController {
    private let userService: UserServiceProtocol
    private let profileView = UserProfileView()
    
    init(userService: UserServiceProtocol) {
        self.userService = userService
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
    
    override func loadView() {
        view = profileView
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        profileView.editButton.addTarget(self, action: #selector(didTapEdit), for: .touchUpInside)
        fetchProfile()
    }
    
    private func fetchProfile() {
        userService.fetchCurrentUser { [weak self] result in
            DispatchQueue.main.async {
                if case .success(let user) = result {
                    self?.profileView.configure(with: user)
                }
            }
        }
    }
    
    @objc private func didTapEdit() {
        // Handle user action or trigger navigation
    }
}
Try answering this question with an AI coach

2What is the role of a provisioning profile in an iOS app release?

In an iOS release, a provisioning profile acts as a cryptographically signed package that bridges Apple's security requirements with the app binary and target devices. Its primary role is to tell the iOS operating system and App Store Connect that the application is authorized to run under specific distribution rules. A provisioning profile bundles together several critical pieces: the App ID (Bundle Identifier), the authorized signing certificate(s) confirming who built the app, the entitlements and capabilities granted to the app (such as Push Notifications, Sign in with Apple, or iCloud), and—in non-App Store profiles like Development or Ad-Hoc—a list of allowed device UDIDs. For App Store release profiles, individual device UDIDs are omitted because Apple allows distribution to any consumer device via the App Store.

Provisioning Profile (.mobileprovision)
├── App ID / Bundle Identifier (e.g., com.example.app)
├── Certificates (Public key / Distribution Certificate)
├── Entitlements (e.g., Push Notifications, Associated Domains)
└── Device UDIDs (Present for Development/Ad-Hoc; omitted for App Store distribution)
Try answering this question with an AI coach

3What does it mean for an iOS app to run work in the background, and what basic limits does the system place on that work?

Running work in the background on iOS means executing code when the app is not currently active on screen or visible to the user. When a user exits an app, it transitions from the active foreground state to the background, and shortly thereafter into a suspended state where its execution is paused, though its memory remains in RAM. To preserve battery life, system responsiveness, and device resources, iOS strictly controls background execution. The system limits how long an app can execute code once backgrounded (typically limited to a short finite window of roughly 30 seconds unless specific background modes or scheduled tasks are used), throttles CPU and network priority, and can terminate suspended apps if the device encounters memory pressure.

import UIKit

NotificationCenter.default.addObserver(
    forName: UIApplication.didEnterBackgroundNotification,
    object: nil,
    queue: .main
) { _ in
    print("App entered background. Code execution will pause soon unless extended.")
}
Try answering this question with an AI coach

4What is a Combine publisher, and how does it differ from a subscriber in a typical iOS data flow?

In Combine, a Publisher emits a stream of values over time and can finish with a completion event (either completing successfully or failing with an error). It declares two associated types: `Output` (the type of data it produces) and `Failure` (the type of error it can emit). A Subscriber receives values and lifecycle events from a publisher. In a typical iOS data flow, publishers act as the source or producer of asynchronous events (such as network responses, notifications, or user inputs), while subscribers act as the consumers that react to emitted values, handle errors, and respond to completion (such as updating UI state or writing to a database). The subscriber attaches to a publisher, receives a subscription token, and requests demand for elements.

import Combine

// Publisher: emits a sequence of integers
let numberPublisher = [1, 2, 3].publisher

// Subscriber: consumes the emitted integers
let cancellable = numberPublisher.sink(
    receiveCompletion: { completion in
        switch completion {
        case .finished:
            print("Finished")
        case .failure(let error):
            print("Error: \(error)")
        }
    },
    receiveValue: { value in
        print("Received: \(value)")
    }
)
Try answering this question with an AI coach

5What does async/await mean in Swift, and how would you use it to call an asynchronous API from an iOS app?

`async/await` is Swift's built-in syntax for writing asynchronous code in a linear, readable, sequential manner rather than using nested closures and completion handlers. Marking a function with `async` tells the compiler that the function can suspend its execution while waiting for long-running work (such as network requests or file I/O) to finish. The `await` keyword indicates a suspension point: execution of the current function pauses, freeing up the underlying thread to do other work, and resumes once the awaited result or error is ready. To call an asynchronous API in an iOS app, you call the `async` method preceded by `await` (and `try await` if the function throws) from within an asynchronous context, such as inside another `async` function or a `Task` block.

func fetchUserData(from url: URL) async throws -> User {
    let (data, response) = try await URLSession.shared.data(from: url)
    
    guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
        throw URLError(.badServerResponse)
    }
    
    return try JSONDecoder().decode(User.self, from: data)
}
Try answering this question with an AI coach

6When would you use UserDefaults in an iOS app, and what kinds of data should not be stored there?

UserDefaults is designed for storing small, lightweight user preferences, settings, and flags across app launches (for example, theme preferences, volume level, or a `hasSeenOnboarding` boolean flag). It supports property list types such as String, Int, Double, Bool, Date, Data, Array, and Dictionary. You should NOT store: 1. Sensitive information (such as user passwords, private keys, or authentication tokens) because UserDefaults stores data unencrypted in a plaintext plist file. 2. Large datasets, documents, or media files (like images, audio, or large JSON payloads) because the entire plist is loaded into memory when accessed, which leads to high memory overhead and slows down app launch.

// Saving simple preference values
UserDefaults.standard.set(true, forKey: "hasCompletedOnboarding")
UserDefaults.standard.set("dark", forKey: "preferredTheme")

// Reading values back
let hasCompletedOnboarding = UserDefaults.standard.bool(forKey: "hasCompletedOnboarding")
let theme = UserDefaults.standard.string(forKey: "preferredTheme") ?? "system"
Try answering this question with an AI coach

7What is ARC in Swift, and how does it manage the lifetime of class instances in an iOS app?

ARC (Automatic Reference Counting) is Swift's compile-time memory management mechanism for tracking and managing the lifetime of class instances (reference types). Every time a new instance of a class is created and assigned to a strong reference, ARC tracks an internal reference count for that instance. As long as at least one strong reference to an instance exists, ARC keeps the instance in memory. When all strong references are removed and the reference count drops to zero, ARC immediately deallocates the instance to free memory, automatically invoking the instance's `deinit` method right before deallocation. Unlike runtime garbage collection in environments like Java or .NET, ARC does not run periodic sweep cycles; the Swift compiler inserts appropriate retain and release calls into the binary at compile time.

class User {
    let name: String
    init(name: String) {
        self.name = name
        print("\(name) initialized")
    }
    deinit {
        print("\(name) deallocated")
    }
}

var ref1: User? = User(name: "Alice") // count = 1
var ref2: User? = ref1               // count = 2

ref1 = nil                           // count = 1
ref2 = nil                           // count = 0 -> deinit called
Try answering this question with an AI coach

8How do you perform a simple HTTP GET request with URLSession in an iOS app?

To perform a simple HTTP GET request in an iOS app, you create a URL object and pass it to a URLSession instance, such as URLSession.shared. You can execute the request using a data task completion handler (URLSession.shared.dataTask(with: url) { data, response, error in ... }) or using modern Swift concurrency (let (data, response) = try await URLSession.shared.data(from: url)). When using the traditional data task approach, you must call .resume() on the task to start it, handle any transport error, inspect the HTTPURLResponse status code, and ensure any UI updates are dispatched to the main thread.

guard let url = URL(string: "https://api.example.com/items") else { return }

let task = URLSession.shared.dataTask(with: url) { data, response, error in
    if let error = error {
        print("Network error: \(error.localizedDescription)")
        return
    }
    
    guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
        print("Server error or invalid status code")
        return
    }
    
    if let data = data {
        DispatchQueue.main.async {
            // Update UI on main thread
        }
    }
}
task.resume()
Try answering this question with an AI coach

9What is Instruments in Xcode, and how would you use it to investigate a simple performance problem in an iOS app?

Instruments is Apple's performance profiling and analysis tool bundled with Xcode. It allows developers to monitor runtime behavior, CPU usage, memory allocations, memory leaks, and UI rendering performance. To investigate a simple performance issue (such as UI stutter or high CPU usage), you launch Instruments from Xcode (via Product -> Profile), choose an appropriate template (such as Time Profiler for CPU bottlenecks or Allocations/Leaks for memory problems), and record a trace while reproducing the problematic user flow on a device. After recording, you analyze the call tree and heaviest stack traces to pinpoint the exact methods causing delays or excessive resource consumption, allowing you to measure and locate the bottleneck before making code changes.

1. In Xcode, select Product -> Profile (Builds with Release optimizations).
2. Select the 'Time Profiler' template.
3. Click Record and perform the sluggish action in the app.
4. Stop recording.
5. Inspect the Call Tree tab:
   - Enable 'Separate by Thread' and 'Hide System Libraries'.
   - Trace the heaviest call stack on the Main Thread to identify the blocking function.
Try answering this question with an AI coach

10What is APNs and what role does it play in delivering push notifications to an iOS app?

APNs (Apple Push Notification service) is Apple's cloud-based intermediary service that securely routes push notifications from your backend servers (provider servers) to Apple devices. Because mobile devices cannot maintain continuous, open socket connections to every individual app backend without draining battery and consuming excessive network bandwidth, Apple maintains a single persistent, low-power connection between each device and APNs. When your app registers for remote notifications, APNs generates a unique, opaque device token identifying that specific app installation on that specific device. The app sends this token to your backend provider server. When the provider wants to notify the user, it constructs a payload and sends it alongside the device token to APNs over HTTP/2. APNs then locates the active connection for that device and delivers the push notification payload directly to iOS, which displays the alert or wakes the app as configured.

[Provider Server] ---> (HTTP/2 Request + Device Token + Payload) ---> [APNs]
                                                                           |
                                                               (Persistent APNs Connection)
                                                                           v
                                                                    [iOS Device / App]
Try answering this question with an AI coach

Intermediate questions

11How would you refactor a large UIViewController that handles UI updates, validation, networking, and navigation?

Refactoring a Massive View Controller (MVC) should be done incrementally to reduce risk while separating distinct responsibilities into dedicated layers: 1. Extract Networking & Data: Move API calls, data persistence, and JSON parsing out of the view controller into dedicated Service or Repository classes with protocol abstractions. 2. Extract Presentation State & Validation: Introduce a ViewModel (or Presenter). Shift input validation, string formatting, and UI state management into the ViewModel, making this logic unit-testable in isolation. 3. Extract Navigation: Adopt the Coordinator pattern (or Router / Flow Controller) to remove push/present calls and navigation flow logic from the view controller. 4. Keep the View Controller Lean: Retain only UI layout, subview configuration, lifecycle hooks, and binding UI controls to the ViewModel. 5. Incremental Execution with Tests: Add unit tests for newly extracted services and ViewModels during the refactor to guarantee existing behavior remains unchanged.

// 1. Extracted Navigation
protocol LoginCoordinatorProtocol: AnyObject {
    func showHomeScreen()
}

// 2. Extracted Business Logic & State
final class LoginViewModel {
    private let authService: AuthServiceProtocol
    private weak var coordinator: LoginCoordinatorProtocol?
    
    init(authService: AuthServiceProtocol, coordinator: LoginCoordinatorProtocol) {
        self.authService = authService
        self.coordinator = coordinator
    }
    
    func login(email: String, password: String) {
        guard email.contains("@") else { return }
        authService.login(email: email, password: password) { [weak self] result in
            if case .success = result { self?.coordinator?.showHomeScreen() }
        }
    }
}

// 3. Lean UIViewController: Only handles UI and bindings
final class LoginViewController: UIViewController {
    private let viewModel: LoginViewModel
    init(viewModel: LoginViewModel) { self.viewModel = viewModel; super.init(nibName: nil, bundle: nil) }
    required init?(coder: NSCoder) { fatalError() }
}
Try answering this question with an AI coach

12How would you troubleshoot an iOS archive upload failure caused by signing or provisioning errors?

Troubleshooting an iOS archive upload failure caused by signing or provisioning issues involves checking three main areas: distribution certificates, provisioning profiles, and target entitlements. 1. Inspect Diagnostic Logs: Review Xcode Organizer distribution logs, detailed upload validation errors, or CI console logs (`altool`/`notarytool`) to identify the exact rejection reason. 2. Certificate & Identity Verification: Verify that a valid Apple Distribution certificate is present in the Keychain alongside its matching private key, and that it has not expired, been revoked, or lacked the Apple WWDR intermediate certificate. 3. Provisioning Profile Matching: Ensure the profile used for export is an App Store Distribution profile matching the exact Bundle Identifier. If capabilities like Push Notifications or Associated Domains are used, verify that an explicit App ID is configured rather than an incompatible wildcard. 4. Entitlements Alignment: Check for discrepancies between the target's `.entitlements` file and the capabilities enabled for the App ID in the Apple Developer Portal. If local entitlements claim permissions not registered on the portal profile, the upload will fail. 5. Signing Configuration: If using automatic signing, verify the selected Team and Apple ID credentials in Xcode Settings. If using manual signing (or CI export options), ensure `ExportOptions.plist` maps each bundle ID to the correct distribution certificate and profile.

# Decode the provisioning profile embedded in the archive
security cms -D -i MyApp.xcarchive/Products/Applications/MyApp.app/embedded.mobileprovision > profile.plist

# Read entitlements embedded in the profile
/usr/libexec/PlistBuddy -c "Print :Entitlements" profile.plist

# Inspect signature and entitlements on the built binary
codesign -d --entitlements :- MyApp.xcarchive/Products/Applications/MyApp.app
Try answering this question with an AI coach

13How would you choose between BGAppRefreshTask, BGProcessingTask, and background URLSession for a production background sync feature?

Choosing the right background API depends on the task duration, whether power/network conditions are required, and the nature of the network payload: 1. **BGAppRefreshTask**: Best for short, lightweight content updates (like refreshing news feeds, social timelines, or user dashboards) that take roughly 15–30 seconds. The system schedules these based on user usage patterns so that fresh content is ready right before the user typically opens the app. 2. **BGProcessingTask**: Designed for long-running, non-urgent, heavy operations such as data indexing, database cleanup/migrations, ML model training, or large data syncs. It can run for minutes and can require the device to be charging (`requiresExternalPower = true`) and connected to Wi-Fi/network (`requiresNetworkConnectivity = true`), typically executing overnight. 3. **Background URLSession (`URLSessionConfiguration.background`)**: The correct choice when transferring large files (uploading photos/videos or downloading large asset bundles) that must continue out-of-process even if the app is suspended or terminated by the OS. It offloads network transfer to the `nsurlsessiond` daemon rather than keeping app code active in memory.

import BackgroundTasks

func scheduleDatabaseMaintenance() {
    let request = BGProcessingTaskRequest(identifier: "com.app.db_cleanup")
    request.requiresNetworkConnectivity = false
    request.requiresExternalPower = true // Run overnight on charger
    request.earliestBeginDate = Date(timeIntervalSinceNow: 6 * 3600)
    
    try? BGTaskScheduler.shared.submit(request)
}

func scheduleTimelineRefresh() {
    let request = BGAppRefreshTaskRequest(identifier: "com.app.feed_refresh")
    request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
    
    try? BGTaskScheduler.shared.submit(request)
}
Try answering this question with an AI coach

14How would you build a Combine pipeline for a search text field that debounces input, ignores duplicates, performs a network request, and updates the UI safely?

To build a safe search pipeline in Combine: 1. **Debounce & Deduplicate:** Take the search query publisher (e.g. `$queryText`), apply `.debounce(for: .milliseconds(300), scheduler: RunLoop.main)` to wait for typing pauses, and `.removeDuplicates()` to ignore unchanged query strings. 2. **Network Request & Cancellation:** Map each query to a network request publisher and flatten using `.switchToLatest()`. This automatically cancels in-flight requests when a new search query arrives, avoiding race conditions and stale results. 3. **Isolate Errors:** Catch network errors inside the inner publisher (e.g. `.catch { _ in Just([]) }`) so network failures do not terminate the outer search text stream. 4. **Main Thread Delivery:** Apply `.receive(on: DispatchQueue.main)` before updating state or assigning to UI properties.

import Combine
import Foundation

class SearchViewModel: ObservableObject {
    @Published var query: String = ""
    @Published var searchResults: [String] = []
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        $query
            .debounce(for: .milliseconds(300), scheduler: RunLoop.main)
            .removeDuplicates()
            .map { query -> AnyPublisher<[String], Never> in
                guard !query.trimmingCharacters(in: .whitespaces).isEmpty else {
                    return Just([]).eraseToAnyPublisher()
                }
                return self.performSearch(query)
                    .catch { _ in Just([]) }
                    .eraseToAnyPublisher()
            }
            .switchToLatest()
            .receive(on: DispatchQueue.main)
            .assign(to: &$searchResults)
    }
    
    private func performSearch(_ term: String) -> AnyPublisher<[String], Error> {
        let url = URL(string: "https://api.example.com/search?q=\(term)")!
        return URLSession.shared.dataTaskPublisher(for: url)
            .map(\.data)
            .decode(type: [String].self, decoder: JSONDecoder())
            .eraseToAnyPublisher()
    }
}
Try answering this question with an AI coach

15How would you use Swift async/await and Task cancellation to implement a screen that loads data from the network and avoids updating the UI after the user navigates away?

I would make the network load an `async` function on a service or view model and run it from a `Task` whose lifetime is tied to the screen. In UIKit, that usually means storing something like `var loadTask: Task<Void, Never>?` on the view controller or view model, starting it when the screen should load, and cancelling it in `viewWillDisappear`, `deinit`, or before starting a newer request depending on the desired lifetime. In SwiftUI, I would prefer `.task` or `.task(id:)` when possible because SwiftUI cancels it when the view disappears or the id changes. Inside the task, I would call the async network API with `try await`, handle cancellation separately from real errors, and check cancellation before applying results if there are multiple awaits or processing steps. UI state changes must happen on the main actor, either by making the view model `@MainActor` or by using `await MainActor.run`. To avoid stale UI, I would cancel previous tasks, avoid detached/global tasks for screen-bound work, and optionally compare a request id or current item id before applying the result.

@MainActor
final class UsersViewController: UIViewController {
    private var loadTask: Task<Void, Never>?
    private let service: UserService

    override func viewDidLoad() {
        super.viewDidLoad()
        loadUsers()
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        loadTask?.cancel()
    }

    private func loadUsers() {
        loadTask?.cancel()
        loadingView.isHidden = false

        loadTask = Task { [weak self] in
            do {
                let users = try await self?.service.fetchUsers() ?? []
                try Task.checkCancellation()

                guard let self else { return }
                self.loadingView.isHidden = true
                self.tableModel = users
                self.tableView.reloadData()
            } catch is CancellationError {
                // User navigated away or a new load replaced this one; do not update UI.
            } catch {
                guard let self else { return }
                self.loadingView.isHidden = true
                self.showError(error)
            }
        }
    }
}
Try answering this question with an AI coach

16How would you choose between UserDefaults, Keychain, files, SQLite, and Core Data for different types of app data?

Choosing the appropriate iOS storage mechanism depends on data sensitivity, size, relational complexity, query requirements, and backup lifecycle: 1. **Keychain**: For sensitive data (auth tokens, passwords, encryption keys, biometric secrets). It provides hardware-backed encryption, persists across app re-installs, and remains secure even in compromised sandbox environments. 2. **UserDefaults**: For lightweight, non-sensitive preferences and flags (e.g., `hasCompletedOnboarding`, theme preference). Because it loads entirely into memory as a property list, it should not be used for large datasets, media, or sensitive tokens. 3. **File System (Documents / Caches / Application Support)**: For discrete files and large binary blobs (downloaded images, audio, PDFs). `Documents` is user-facing and backed up to iCloud; `Caches` is for purgeable re-downloadable content; `Application Support` is for non-user-facing persistent files. 4. **SQLite**: For structured, tabular data needing fast indexed queries, bulk operations, or cross-platform database sharing without object-graph overhead. 5. **Core Data / SwiftData**: For complex object graphs with relationships, change tracking, faulting, undo management, and direct integration with UIKit (`NSFetchedResultsController`) or SwiftUI (`@Query`).

- Auth tokens / API keys          -> Keychain (Hardware-backed encrypted store)
- App settings / UI flags         -> UserDefaults (Lightweight key-value, non-sensitive)
- Downloaded videos / PDFs        -> File System (Documents / Caches directory)
- 50k items with complex queries   -> SQLite / Core Data / SwiftData (Indexed, relationship-aware)
Try answering this question with an AI coach

17In a production UIKit screen with callbacks from a view model, how would you decide whether to capture self as weak, unowned, or strong in closure handlers?

In UIKit architecture where a view controller interacts with a view model via callbacks, the capture strategy is determined by ownership and lifecycle: 1. `[weak self]`: The standard and safest approach for stored or escaping closures (such as view model event callbacks or asynchronous network/data completion handlers). Because the view controller owns the view model, capturing `self` strongly in a callback stored by the view model creates a strong reference cycle (`VC -> VM -> closure -> VC`). `[weak self]` converts `self` into an optional (`UIViewController?`), allowing `self` to deallocate cleanly and preventing memory leaks. 2. `[unowned self]`: Assumes `self` will never be nil when the closure executes. It should be used with extreme caution in UI screens. If an asynchronous task or callback completes after the view controller has been dismissed and deallocated, accessing `unowned self` causes a fatal runtime crash. For this reason, `[weak self]` is generally preferred over `unowned` in production UIKit UI callbacks. 3. Strong capture (default, no capture list): Appropriate when the closure is non-escaping (e.g., standard collection operations like `map`/`filter`) or when the closure is short-lived and does not create an ownership cycle back to `self`.

final class ProfileViewController: UIViewController {
    private let viewModel: ProfileViewModel

    init(viewModel: ProfileViewModel) {
        self.viewModel = viewModel
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) { fatalError() }

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // VM stores the callback -> capture weak self to break retain cycle
        viewModel.onDataUpdated = { [weak self] state in
            guard let self else { return }
            self.updateUI(with: state)
        }
    }

    private func updateUI(with state: ProfileState) { /* Update views */ }
}
Try answering this question with an AI coach

Advanced questions

18You are joining a mature iOS app with hundreds of screens, many massive view controllers, and slow release cycles. How would you plan an incremental architecture improvement strategy without stopping feature delivery?

An effective incremental migration strategy avoids full rewrites and pairs refactoring directly with ongoing feature delivery (following the Strangler Fig pattern). You begin by establishing an agreed-upon target architecture blueprint (such as MVVM or VIPER with dependency injection and modular coordinators) and establishing test baselines (characterization, snapshot, and unit tests) around legacy code before touching it. Prioritization should be driven by feature churn and risk: refactor screens that teams are actively modifying or areas with high bug rates, rather than stable legacy code. When refactoring Massive View Controllers, decouple business and presentation logic into dedicated ViewModels/Presenters and introduce protocol-based adapters or coordinators to isolate legacy UI from new code. Finally, maintain velocity and quality through architectural governance: provide golden sample reference implementations, enforce boundaries via CI linters or PR guidelines, and allocate continuous technical debt capacity (e.g., 15–20% capacity or pairing refactors with related feature work) while tracking metrics like build times, crash rates, and test coverage.

protocol ProfileDisplaying: AnyObject {
    func updateProfile(name: String, avatarUrl: URL?)
}

extension LegacyProfileViewController: ProfileDisplaying {
    func updateProfile(name: String, avatarUrl: URL?) {
        self.nameLabel.text = name
    }
}

final class ProfilePresenter {
    private weak var view: ProfileDisplaying?
    private let userUseCase: FetchUserUseCaseProtocol
    
    init(view: ProfileDisplaying, userUseCase: FetchUserUseCaseProtocol) {
        self.view = view
        self.userUseCase = userUseCase
    }
    
    func onViewLoaded() async {
        guard let user = try? await userUseCase.execute() else { return }
        await MainActor.run {
            view?.updateProfile(name: user.name, avatarUrl: user.avatarURL)
        }
    }
}
Try answering this question with an AI coach

19You are releasing a high-traffic iOS app with a database migration and a backend API change; how would you design the rollout plan to minimize user impact?

Designing a rollout plan for a high-traffic iOS app involving database migrations and backend API changes requires decoupling deployment from feature activation due to iOS client constraints (asynchronous user updates and inability to force client-side binary rollbacks). 1. Backend Compatibility: Implement an expand-and-contract strategy. Deploy API v2 alongside API v1 so that both legacy and updated client versions function concurrently without breaking changes. 2. Resilient Database Migration: Ensure local schema migrations (e.g., SQLite, Core Data, SwiftData) are idempotent, non-destructive, and safely handle multi-version jumps (e.g., upgrading from N-3 to N) without blocking the main thread or causing launch crash loops. 3. Dynamic Gating: Ship the new client features dark behind server-driven feature flags/remote configuration. Keep the feature flag disabled during initial distribution. 4. Phased Rollout & Monitoring: Distribute the app via App Store Phased Release (a 7-day staged rollout). Continuously monitor telemetry, crash rates, database migration success metrics, and API error rates. If anomalies occur, pause the phased rollout in App Store Connect and toggle feature flags off without requiring an emergency binary rollback.

Phase 1 (Backend Expand): Deploy API v2 supporting both new and legacy payloads.
Phase 2 (Client Distribution & Migration): Release client v2.0 via App Store Phased Release (1% -> 100%). DB migrates safely on first launch. Feature flag remains OFF.
Phase 3 (Validation & Feature Enablement): Monitor crash rates & migration success. Incrementally ramp Remote Config flag (10% -> 50% -> 100%).
Phase 4 (Backend Contract): Once legacy app adoption drops below deprecation threshold, sunset API v1.
Try answering this question with an AI coach

20You are designing offline-first sync for a notes app where edits must eventually sync across devices, but iOS may defer or cancel background work. What background execution architecture would you choose?

An offline-first sync architecture treats the local database (such as SQLite, Core Data, or SwiftData) as the immediate single source of truth for UI state, while mutations are appended to a persistent outbox queue on disk so that unsent edits survive process death. To manage iOS's opportunistic and non-deterministic background execution, background work is structured across multiple tiers: 1. Outbox Flush on Backgrounding: Initiated via `UIApplication.shared.beginBackgroundTask(expirationHandler:)` to finish in-flight or quick pending mutations. 2. Scheduled Periodic Sync: Registered with `BGTaskScheduler` using `BGAppRefreshTask` for lightweight metadata/delta sync and `BGProcessingTask` for heavier sync operations. 3. Large Asset Sync: Handled out-of-process via a background `URLSessionConfiguration` using file uploads/downloads. 4. Server-Triggered Sync: Opportunistic wakeups via silent push notifications (`content-available: 1`). Because background tasks can be interrupted or rescheduled at any point, operations must use client-generated idempotency keys (e.g., UUIDs) to allow safe retries without duplicating data. If a task's `expirationHandler` is called, in-flight work must be cancelled immediately, uncommitted operations marked back to pending, and `setTaskCompleted(success: false)` called. Eventual consistency is maintained using conflict resolution mechanisms such as CRDTs, version vectors, or last-write-wins with deletion tombstones, ensuring remote deltas do not overwrite uncommitted local outbox mutations.

import BackgroundTasks
import Foundation

final class NoteSyncCoordinator {
    static let shared = NoteSyncCoordinator()
    private let syncTaskID = "com.notesapp.sync.refresh"

    func registerSyncTask() {
        BGTaskScheduler.shared.register(forTaskWithIdentifier: syncTaskID, using: nil) { task in
            guard let refreshTask = task as? BGAppRefreshTask else { return }
            self.handleAppRefresh(task: refreshTask)
        }
    }

    func scheduleNextSync() {
        let request = BGAppRefreshTaskRequest(identifier: syncTaskID)
        request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
        try? BGTaskScheduler.shared.submit(request)
    }

    private func handleAppRefresh(task: BGAppRefreshTask) {
        scheduleNextSync()
        let syncTask = Task {
            do {
                try await SyncEngine.shared.flushPersistentOutboxAndFetchDeltas()
                task.setTaskCompleted(success: true)
            } catch {
                task.setTaskCompleted(success: false)
            }
        }

        task.expirationHandler = {
            syncTask.cancel()
        }
    }
}
Try answering this question with an AI coach