iOS Developer Interview Questions and Answers

iOS developer interview questions for 2026: Swift, SwiftUI, UIKit, ARC, async/await, actors, MVVM, networking, Core Data, XCTest, and Apple/Google iOS loop prep with sample answers.

Published

Updated

Tech reviewed byDeepak Prasad

iOS Developer Interview Questions and Answers

iOS developer interviews in 2026 test whether you can ship reliable apps on Apple’s stack—Swift value semantics, ARC discipline, SwiftUI state, and Swift 6 concurrency—not whether you can recite every UIKit delegate method. Loops at Apple, Google (iOS apps on the App Store), banks, and startups alike push async/await, actors, and architecture trade-offs alongside UI fundamentals.

Below are 45 questions for iOS developer and senior mobile engineer roles: Swift language depth, UIKit and SwiftUI, memory, concurrency, persistence, networking, testing, and scenario design. Open each answer after you try the question yourself. For the Android side of mobile prep, see senior Android developer interview questions. For shared OOP and threading vocabulary, skim Java interview questions (part 1).

NOTE
Prep target: In 2026, expect SwiftUI + concurrency even in UIKit-heavy codebases. Be ready to compare GCD vs structured concurrency, explain @MainActor, and walk one offline-first or list performance scenario aloud.

Interview context and how to prepare

What do iOS developer interviews actually test?

iOS interviews test whether you can build production apps on Apple platforms—not syntax trivia alone.

Level Typical focus
Junior Swift basics, optionals, UITableView/UICollectionView or SwiftUI lists, Auto Layout basics
Mid ARC, protocols, networking, MVVM, persistence, unit tests
Senior Concurrency (async/await, actors), performance, modularization, app architecture, system design
Staff+ Cross-team API design, Swift 6 migration, release strategy, mentoring

Common formats:

  • Phone screen → take-home or live coding → system design (senior) → behavioral
  • Apple loops: deep Swift, frameworks, craftsmanship, collaboration
  • Google iOS loops: algorithms + mobile depth + cross-platform empathy (compare with their stack when asked)

A strong candidate explains trade-offs (UIKit vs SwiftUI, Core Data vs SwiftData) with project context.

Apple iOS developer vs Google iOS developer interviews — what differs?

Both hire engineers to ship App Store products; emphasis varies:

Signal Apple-heavy loops Google-heavy loops
Language Swift/Obj-C depth, Apple APIs, HIG Swift + strong CS/algorithms
UI SwiftUI/UIKit craft, accessibility Scale, metrics, A/B, performance
Process Craft, API design, privacy Structured interviews, coding rigor
Platform Latest OS features, WWDC direction Large-app architecture, shared libraries

Same prep core: Swift, ARC, concurrency, networking, architecture, testing.

Differentiate answers with product scale (1M DAU vs internal tool) and release cadence you have shipped.

Do not assume one company’s folklore—read the team (consumer app vs enterprise SDK).

What is a typical iOS developer interview loop?
Round Duration Focus
Recruiter / HM 30 min Apps shipped, App Store link, stack
Swift / iOS fundamentals 45–60 min Types, ARC, protocols, UI lifecycle
UI & architecture 45–60 min SwiftUI state or UIKit patterns, MVVM
Live coding 45–90 min Small feature, API client, table/list problem
System design 45–60 min Feed, chat, offline sync—senior roles
Behavioral 30–45 min Crashes, deadlines, code review

Take-home tasks: build a small app (network + list + detail) with README and tests.

Practice explaining answers aloud; interviewers follow up on spoken reasoning more than memorized flashcards.

What is a realistic 4–6 week prep plan?
Week Focus Output
1 Swift — optionals, structs/classes, protocols, enums 20 small Playground exercises
2 ARC — weak/unowned, closures, retain cycles Fix three leak scenarios on paper
3 UI — SwiftUI state or UIKit table + navigation Ship one screen with loading/error states
4 Concurrency — async/await, Task, @MainActor Refactor one callback API to async
5 Data + network — URLSession, Codable, persistence choice Mini app with cache
6 Tests + scenarios — XCTest, offline design, STAR stories One architecture whiteboard

Pair mobile prep with senior Android interviews for cross-platform system design vocabulary.


Swift language fundamentals

Struct vs class — when do you use each?

Swift has both value types and reference types.

Area Struct Class
Type category Value type Reference type
Assignment Creates an independent value Shares the same instance
Identity No object identity Has identity with ===
Inheritance Not supported Supported
ARC Struct itself is not reference-counted Managed by ARC
Mutation Controlled with mutating methods Mutable through references
Common use Models, values, DTOs, SwiftUI views View controllers, delegates, shared services

Prefer structs when the data represents a value:

  • API response models
  • Form data
  • App state snapshots
  • Coordinates, sizes, dates, money-like values
  • SwiftUI View types

Use classes when you need identity or shared mutable state:

  • UIViewController
  • Delegate objects
  • Shared cache/session managers
  • Objects that need inheritance
  • Objects with lifecycle/deinitialization behavior

Example:

swift
struct UserProfile {
    var name: String
}

final class ImageCache {
    private var storage: [URL: UIImage] = [:]
}

Interview follow-up: Swift collections like Array, String, and Dictionary are value types but use copy-on-write internally, so copying is usually efficient until one copy is mutated.

Important nuance: a struct can contain a class reference. In that case, the struct value is copied, but the reference inside it may still point to shared mutable state.

A strong answer is:

“I use structs by default for data because value semantics reduce shared-state bugs. I use classes when I need identity, reference semantics, inheritance, Objective-C/UIKit interop, or a controlled lifecycle.”

Optional binding: if let vs guard let?

Both if let and guard let safely unwrap optionals, but they communicate different intent.

swift
// if let — use value only inside this branch
if let name = user?.name {
    print(name)
}

// guard let — required value; exit early if missing
func greet(_ user: User?) {
    guard let name = user?.name else { return }
    print("Hello, \(name)")
}
Prefer guard let Prefer if let
Value is required to continue Value is optional for one branch
You want early exit You have success/failure branches
You want less nesting You only need value in a small scope
Used in functions, handlers, validation Used for conditional UI or optional behavior

Example interview explanation:

swift
func submit(user: User?) {
    guard let user else {
        showLogin()
        return
    }

    save(user)
}

Here, guard makes the happy path clear. After the guard statement, user is safely unwrapped for the rest of the function.

Avoid force unwraps in normal production paths:

swift
// Risky
let name = user!.name

Force unwrap is acceptable only when nil is truly impossible and you can justify why, such as a storyboard outlet after view loading or a test setup where failure should crash loudly.

Implicitly unwrapped optionals like String! still appear in UIKit/Objective-C interop, but they should not be used casually.

A strong answer is:

“I use guard let when the value is required and I want to exit early. I use if let when the optional value only matters for one conditional branch. I avoid force unwrap unless nil is impossible by design.”

What are enums with associated values good for?

Swift enums with associated values let each case carry different data.

They are useful for modeling state, events, and results safely.

swift
enum LoadState<T> {
    case idle
    case loading
    case loaded(T)
    case failed(Error)
}

This is better than multiple unrelated booleans:

swift
var isLoading = false
var data: [User]?
var error: Error?

With separate flags, invalid states are possible:

  • isLoading == true and data != nil
  • error != nil and data != nil
  • Not loading, no data, no error, but unclear state

With an enum, the state is explicit:

swift
switch state {
case .idle:
    showEmpty()
case .loading:
    showSpinner()
case .loaded(let users):
    showUsers(users)
case .failed(let error):
    showError(error)
}

Common interview use cases:

  • API loading state
  • Login state
  • Navigation flow
  • Form submission state
  • Deep link handling
  • Network result modeling
  • UI state machines

Swift checks switch exhaustiveness, so if you add a new enum case later, the compiler can force you to handle it.

A strong answer is:

“Enums with associated values help model state clearly. Instead of juggling booleans and optional values, I can represent each valid state directly and let the compiler enforce exhaustive handling.”

What is protocol-oriented programming?

Protocol-oriented programming means designing around capabilities instead of deep inheritance trees.

A protocol defines what a type can do:

swift
protocol FeedRepository {
    func fetchFeed() async throws -> [FeedItem]
}

Different types can conform:

swift
struct RemoteFeedRepository: FeedRepository {
    func fetchFeed() async throws -> [FeedItem] {
        // call API
    }
}

struct MockFeedRepository: FeedRepository {
    func fetchFeed() async throws -> [FeedItem] {
        []
    }
}

Why it helps:

  • Improves testability
  • Supports dependency injection
  • Avoids deep class inheritance
  • Lets structs, classes, enums, and actors share behavior
  • Makes code depend on abstraction, not concrete implementation

Protocol extensions can provide default behavior:

swift
extension FeedRepository {
    func refresh() async throws -> [FeedItem] {
        try await fetchFeed()
    }
}

Interviewers often ask where protocols help in iOS apps:

  • Mocking API clients in unit tests
  • Abstracting persistence
  • Swapping real services with fake services
  • Building reusable view models
  • Avoiding tightly coupled UIKit/SwiftUI code

Important nuance: protocol extensions are powerful, but they are not the same as overriding class methods. Dynamic dispatch behavior can differ depending on whether the method is a protocol requirement.

Also know modern Swift syntax:

  • some Protocol means one hidden concrete type chosen by the function
  • any Protocol means a boxed existential that can hold different conforming types

A strong answer is:

“Protocol-oriented programming means I define behavior with protocols and compose types around those capabilities. It helps with testability, dependency injection, and avoiding unnecessary inheritance.”

Why are generics useful in Swift?

Generics let you write reusable code without losing type safety.

swift
func first<T>(_ items: [T]) -> T? {
    items.first
}

Without generics, you may reach for Any, which loses compile-time type information:

swift
func first(_ items: [Any]) -> Any? {
    items.first
}

Generics are used heavily in Swift and Apple APIs:

API Generic meaning
Array<Element> Array of a specific element type
Dictionary<Key, Value> Key/value types are known
Optional<Wrapped> Wrapped value may or may not exist
Result<Success, Failure> Success and failure types are explicit
Task<Success, Failure> Async task result types are known

Generic constraints make code reusable but still safe:

swift
func printIDs<T: Identifiable>(_ items: [T]) {
    for item in items {
        print(item.id)
    }
}

Interview follow-ups:

  • Associated types in protocols
  • Generic constraints with where
  • some opaque return types
  • any existential types
  • Type erasure when concrete generic types become too complex

Example answer for some vs any:

Syntax Meaning
some View Function returns one specific hidden concrete type
any FeedRepository Variable can hold any conforming repository type

A strong answer is:

“Generics let me write reusable code while preserving type information. I use them when the algorithm is the same but the data type changes, and I add constraints when the generic type must provide specific behavior.”

What does Codable do and what pitfalls appear in interviews?

Codable is a type alias for Encodable and Decodable.

It lets Swift types convert to and from formats such as JSON or property lists.

swift
struct UserDTO: Codable {
    let id: UUID
    let name: String
    let joinedAt: Date
}

Common JSON decoding:

swift
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601

let user = try decoder.decode(UserDTO.self, from: data)

Common interview pitfalls:

Problem Solution
JSON key does not match Swift property Use CodingKeys
Snake case keys Use keyDecodingStrategy carefully
Date format mismatch Set dateDecodingStrategy
Missing optional field Use optional property or decodeIfPresent
Missing required field Let decoding fail or provide custom decoding
API model differs from app model Use DTO → domain mapping
Backend changes response shape Add backward-compatible decoding
Huge payload Decode only needed fields or redesign API

Example key mapping:

swift
struct UserDTO: Codable {
    let userID: Int
    let fullName: String

    enum CodingKeys: String, CodingKey {
        case userID = "user_id"
        case fullName = "full_name"
    }
}

A strong answer should not say “Codable always handles JSON automatically.” It handles simple cases well, but real APIs often need custom keys, date strategies, optional handling, and version-tolerant decoding.

For production apps, consider separating:

  • DTO model — matches API response
  • Domain model — matches app business logic
  • View model — matches screen needs

A strong answer is:

“Codable gives type-safe encoding and decoding, but real APIs need careful handling of key names, dates, missing fields, and DTO-to-domain mapping. I do not let backend JSON shape leak everywhere into the app.”

Value type vs reference type — practical interview angle?

The practical difference is how changes spread through the app.

A value type gives each variable its own independent value.

swift
struct Settings {
    var theme: String
}

var a = Settings(theme: "Light")
var b = a
b.theme = "Dark"

print(a.theme) // Light
print(b.theme) // Dark

A reference type lets multiple variables point to the same instance.

swift
final class SettingsStore {
    var theme = "Light"
}

let a = SettingsStore()
let b = a
b.theme = "Dark"

print(a.theme) // Dark
print(b.theme) // Dark

Why interviewers care:

  • Value types reduce accidental shared mutation
  • Reference types are useful for shared identity
  • Classes are managed by ARC
  • Reference cycles can cause memory leaks
  • Value semantics make state easier to reason about
  • Reference semantics are often needed for delegates, controllers, caches, and long-lived services

In SwiftUI, View types are structs because the view is a description of UI for a given state. Long-lived state usually belongs in a model object, observable type, actor, or service.

Modern SwiftUI note:

  • Older apps often use ObservableObject with @Published
  • Newer SwiftUI code may use the Observation framework with @Observable

Value vs reference also affects concurrency. Value types are often easier to pass safely, while shared mutable class instances need more care. Actors are reference types but protect their mutable state through actor isolation.

A strong answer is:

“Value types are best when I want predictable independent state. Reference types are best when identity, shared lifecycle, or shared mutable state matters. In iOS, I use value models for data and reference types for controllers, services, delegates, actors, and observable state owners.”


Memory management and ARC

How does ARC work in Swift?

ARC means Automatic Reference Counting. Swift uses ARC to manage the memory of class instances.

When something holds a strong reference to a class instance, ARC keeps that instance alive. When the last strong reference goes away, ARC deallocates the instance and calls deinit.

swift
final class UserSession {
    deinit {
        print("UserSession deallocated")
    }
}

var session: UserSession? = UserSession()
session = nil // deinit runs when no strong references remain

Important points:

  • Strong references are the default
  • ARC is deterministic, unlike garbage collection
  • deinit runs when the last strong reference is released
  • Structs and enums are value types, not ARC-managed class instances
  • Reference cycles can prevent deinit from running
  • weak and unowned references help break cycles

Common retain cycle:

swift
final class Parent {
    var child: Child?
}

final class Child {
    var parent: Parent?
}

If both references are strong, neither object can be released.

Fix:

swift
final class Child {
    weak var parent: Parent?
}

Interviewers do not only want the definition of ARC. They want to know whether you understand why memory leaks still happen in Swift.

ARC is automatic, but it is not magic. It cannot automatically solve strong reference cycles.

A strong answer is:

“ARC tracks strong references to class instances. When the last strong reference goes away, the object is deallocated. The main risk is a retain cycle, so I use weak or unowned where ownership should not be strong.”

Weak vs unowned — when to use each?

Use weak and unowned to avoid strong reference cycles, but they have different safety rules.

Area weak unowned
Keeps object alive? No No
Becomes nil? Yes No
Type Optional Usually non-optional
Safety Safer default Can crash if object is gone
Common use Delegates, closures, parent references Same lifetime guaranteed
Risk Need optional handling Runtime crash if assumption is wrong

Use weak when the referenced object may be deallocated first.

swift
protocol LoginViewControllerDelegate: AnyObject {
    func loginDidFinish()
}

final class LoginViewController: UIViewController {
    weak var delegate: LoginViewControllerDelegate?
}

Use [weak self] in escaping closures when the closure may outlive the object.

swift
network.fetchUser { [weak self] result in
    guard let self else { return }
    self.handle(result)
}

Use unowned only when the referenced object is guaranteed to live at least as long as the reference.

swift
final class Customer {
    let card: CreditCard

    init(card: CreditCard) {
        self.card = card
    }
}

final class CreditCard {
    unowned let customer: Customer

    init(customer: Customer) {
        self.customer = customer
    }
}

In real app code, weak is usually the safer interview answer unless you can clearly prove the lifetime relationship.

Avoid this unless justified:

swift
api.load { [unowned self] in
    self.updateUI()
}

If the view controller is dismissed before the API finishes, this can crash.

A strong answer is:

“I use weak when the reference can become nil, such as delegates and escaping closures. I use unowned only when the other object is guaranteed to outlive the reference. If I cannot prove that lifetime, I choose weak.”

How do you find and fix a retain cycle?

A retain cycle happens when two or more objects keep each other alive through strong references.

Common sources:

  • Closure captures self strongly
  • Parent holds child and child holds parent
  • Delegate is strong instead of weak
  • Timer or CADisplayLink retains its target
  • ViewModel owns a callback that captures ViewController
  • Combine subscription captures self and is stored by self
  • Long-running async task keeps an object alive unexpectedly

Example closure cycle:

swift
final class ProfileViewModel {
    var onUpdate: (() -> Void)?

    func refresh() {
        onUpdate?()
    }
}

final class ProfileViewController: UIViewController {
    let viewModel = ProfileViewModel()

    override func viewDidLoad() {
        super.viewDidLoad()

        viewModel.onUpdate = {
            self.updateUI()
        }
    }
}

Here, the view controller owns the view model. The view model owns the closure. The closure owns self.

Fix:

swift
viewModel.onUpdate = { [weak self] in
    self?.updateUI()
}

For delegates:

swift
weak var delegate: SomeDelegate?

For timers:

  • Invalidate the timer
  • Use a weak target pattern
  • Use closure-based APIs carefully with [weak self]

How to verify:

  • Add deinit logs during debugging
  • Use Xcode Memory Graph Debugger
  • Use Instruments Leaks/Allocations
  • Navigate away from the screen and confirm objects deallocate
  • Check whether view controllers remain in memory after dismissal

Interview scenario answer:

“If a dismissed view controller is not deallocated, I would check closures, delegates, timers, and subscriptions. Then I would verify the fix with Memory Graph or Instruments, not just assume [weak self] solved it.”

A strong answer is:

“I find retain cycles by checking objects that should deallocate but do not. I usually inspect closures, delegates, timers, and subscriptions, then fix ownership with weak, cleanup, or cancellation, and verify using Xcode Memory Graph or Instruments.”

What is copy-on-write (COW)?

Copy-on-write is an optimization used by Swift value types such as Array, String, Dictionary, and Set.

The idea is simple:

  1. Two values can share the same underlying storage
  2. As long as nobody mutates, no real copy is needed
  3. When one value mutates, Swift makes a separate copy if storage is shared

Example:

swift
var a = [1, 2, 3]
var b = a        // storage can be shared

b.append(4)      // copy happens before mutation if needed

print(a)         // [1, 2, 3]
print(b)         // [1, 2, 3, 4]

Why it matters:

  • Value semantics stay predictable
  • Large values are not always copied immediately
  • Performance stays good for many common cases
  • Mutation can still trigger a real copy

Interviewers may ask this because candidates often say “structs are always copied.” That is conceptually true at the value level, but Swift can optimize storage internally.

Senior follow-up: custom COW types can use reference storage internally and check uniqueness before mutation.

swift
isKnownUniquelyReferenced(&storage)

If storage is uniquely referenced, mutate it directly. If not, copy the storage first.

Common performance note:

  • Passing arrays around is usually cheap
  • Repeated mutation of shared storage can become expensive
  • Large structs with reference-type properties need careful design
  • Copy-on-write is an implementation optimization, not a reason to ignore data ownership

A strong answer is:

“Copy-on-write lets Swift value types share storage until mutation. It preserves value semantics while avoiding unnecessary copies, but mutation of shared storage can still trigger a real copy.”


Architecture and design patterns

MVC vs MVVM on iOS?

iOS apps have historically used MVC, but many production teams use MVVM or related patterns to keep screens testable and maintainable.

Pattern Practical meaning on iOS
MVC Model, View, ViewController
MVVM View, ViewModel, Model
VIPER / Clean More layers for large teams and strict boundaries
TCA / Redux-like State/action-driven architecture, often SwiftUI-friendly

In Apple-style MVC:

  • Model holds data/business rules
  • View displays UI
  • ViewController coordinates the view and model

The problem is that UIViewController often becomes too large. It may handle:

  • View lifecycle
  • API calls
  • Formatting
  • Validation
  • Navigation
  • Error handling
  • Analytics
  • Table/collection delegates

This is why people joke that MVC becomes “Massive View Controller.”

MVVM helps by moving presentation logic into a ViewModel.

swift
@Observable
final class ProfileViewModel {
    var name = ""
    var isLoading = false

    func load() async {
        // fetch and map data for the view
    }
}

SwiftUI works naturally with observable state. UIKit teams also use MVVM, often with closures, delegates, Combine, async/await, or coordinators.

Use MVVM for:

  • Testable presentation logic
  • Screens with loading/error/content states
  • API-backed screens
  • Complex form validation
  • Shared UI state

But do not over-architect tiny screens. The pattern should reduce complexity, not add ceremony.

A strong answer is:

“MVC is common in UIKit, but ViewControllers can become too large. I use MVVM when moving presentation state and formatting into a testable ViewModel improves maintainability. For larger apps, I choose architecture based on team size, testing needs, and navigation complexity.”

What is the delegate pattern and where is it used?

The delegate pattern lets one object hand off responsibility to another object through a protocol.

It is common in UIKit and Apple frameworks.

Examples:

  • UITableViewDelegate
  • UICollectionViewDelegate
  • URLSessionDelegate
  • CLLocationManagerDelegate
  • UITextFieldDelegate

Simple example:

swift
protocol LoginViewControllerDelegate: AnyObject {
    func loginViewControllerDidFinish(_ controller: LoginViewController)
}

final class LoginViewController: UIViewController {
    weak var delegate: LoginViewControllerDelegate?

    func didTapDone() {
        delegate?.loginViewControllerDidFinish(self)
    }
}

Why weak?

The parent often owns the child view controller. If the child strongly owns its delegate, both can keep each other alive.

swift
weak var delegate: LoginViewControllerDelegate?

Delegates are good for:

  • One-to-one communication
  • Parent-child screen callbacks
  • Custom UI components
  • Framework callbacks
  • Situations where the receiver should be explicit and typed

Delegate limitations:

  • Usually only one receiver
  • Can become verbose
  • Not ideal for broadcasting global app events
  • Too many delegate methods can make APIs heavy

SwiftUI uses fewer UIKit delegates for UI, but delegate patterns still matter because many system frameworks and UIKit APIs use them heavily.

A strong answer is:

“A delegate is a typed one-to-one callback relationship defined by a protocol. I usually make delegates weak to avoid retain cycles, and I use them for parent-child UI communication or framework callbacks.”

Delegates vs NotificationCenter?

Delegates and NotificationCenter are both communication patterns, but they solve different problems.

Area Delegate NotificationCenter
Relationship Usually one-to-one One-to-many broadcast
Type safety Strong protocol methods Notification name + payload
Coupling More direct Looser
Traceability Easier to follow Can become hard to trace
Common use Parent-child callbacks, UI events App-wide events
Return value Possible through delegate method No direct response

Use a delegate when one specific object should handle an event.

swift
delegate?.loginViewControllerDidFinish(self)

Use NotificationCenter when many parts of the app may need to react to the same event.

swift
NotificationCenter.default.post(name: .userDidLogout, object: nil)

Good NotificationCenter use cases:

  • User logged out
  • Theme changed
  • Language changed
  • App-wide data refresh needed
  • System notifications

Bad use cases:

  • Passing data between two closely related screens
  • Replacing clear dependency flow
  • Creating hidden app-wide event spaghetti
  • Using raw string names everywhere

Safer pattern:

swift
extension Notification.Name {
    static let userDidLogout = Notification.Name("userDidLogout")
}

Modern alternatives:

  • Combine publishers
  • Async sequences
  • SwiftUI environment/state
  • Direct dependency injection
  • Store/action-based architecture

A strong answer is:

“I use delegates for typed one-to-one communication and NotificationCenter for one-to-many broadcasts. I avoid NotificationCenter for normal screen-to-screen data flow because it can hide dependencies and become difficult to debug.”

How do you do dependency injection on iOS?

Dependency injection means a type receives its dependencies instead of creating them internally.

This improves:

  • Testability
  • Reusability
  • Loose coupling
  • Mocking/faking in tests
  • Clear ownership

Prefer initializer injection for required dependencies.

swift
protocol FeedRepository {
    func fetchFeed() async throws -> [FeedItem]
}

final class FeedViewModel {
    private let repository: FeedRepository

    init(repository: FeedRepository) {
        self.repository = repository
    }
}

In tests:

swift
struct MockFeedRepository: FeedRepository {
    func fetchFeed() async throws -> [FeedItem] {
        [FeedItem(title: "Test")]
    }
}

Common DI styles on iOS:

Approach Use case
Initializer injection Required dependencies
Property injection Optional dependencies or UIKit storyboard cases
Factory/builder Screen construction
SwiftUI environment Shared app-level dependencies
Protocol abstraction Testing and swapping implementations
DI container Large modular apps

Avoid creating dependencies inside the object when they need to be mocked.

swift
// Harder to test
final class FeedViewModel {
    private let repository = RemoteFeedRepository()
}

Better:

swift
final class FeedViewModel {
    private let repository: FeedRepository

    init(repository: FeedRepository) {
        self.repository = repository
    }
}

Singletons are not always wrong, but they should be used carefully. A global singleton can make tests order-dependent and hide dependencies.

Use singletons only for truly global system-like services or when the framework already exposes one, such as UserDefaults.standard or NotificationCenter.default.

A strong answer is:

“I prefer initializer injection with protocols because it makes dependencies explicit and easy to replace in tests. I use factories or SwiftUI environment for app wiring, and I avoid unnecessary singletons because they hide dependencies.”

What is the coordinator pattern?

The coordinator pattern moves navigation logic out of view controllers.

In UIKit, a coordinator usually owns a navigation controller and decides which screen comes next.

swift
final class AppCoordinator {
    private let navigationController: UINavigationController

    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }

    func start() {
        showLogin()
    }

    private func showLogin() {
        let viewController = LoginViewController()
        viewController.delegate = self
        navigationController.pushViewController(viewController, animated: true)
    }
}

Without a coordinator, view controllers often know too much:

  • Which screen comes next
  • How to create the next screen
  • Which dependencies the next screen needs
  • How to handle login/logout flow
  • How to switch tabs or reset navigation

With a coordinator:

  • ViewControllers stay focused on UI
  • Navigation flow is easier to test
  • Deep flows are easier to manage
  • Screen creation is centralized
  • Dependencies can be injected during screen construction

Common coordinator structure:

text
AppCoordinator
  LoginCoordinator
  MainTabCoordinator
  SettingsCoordinator

UIKit use cases:

  • Login/onboarding flows
  • Checkout flows
  • Multi-step forms
  • Deep navigation graphs
  • Apps with many screens

SwiftUI does not usually use the classic UIKit coordinator in the same way. Similar goals can be achieved with:

  • NavigationStack
  • NavigationPath
  • Router object
  • Enum-based routes
  • Observable navigation state

Example SwiftUI route idea:

swift
enum Route: Hashable {
    case profile(User.ID)
    case settings
}

Then navigation state can be driven by a path/router.

Coordinator trade-off:

  • Helpful for complex navigation
  • Extra ceremony for small apps
  • Can become another “god object” if not split by flow

A strong answer is:

“A coordinator owns navigation flow so view controllers do not create and push every next screen themselves. I use it in UIKit apps with complex navigation, while in SwiftUI I usually model similar routing with NavigationStack, routes, and navigation state.”


UIKit essentials

UIKit view controller lifecycle — what matters in interviews?

UIKit view controller lifecycle questions test whether you know where to put setup, refresh, layout, and cleanup code.

Method When it runs Common use
loadView When creating the view manually Build root view without storyboard/xib
viewDidLoad Once after view is loaded One-time setup, constraints, bindings
viewWillAppear Before view appears each time Refresh visible state, navigation bar setup
viewIsAppearing While the view is being added Appearance-related updates with safer timing
viewDidAppear After view is visible Start animations, analytics, focus
viewWillDisappear Before leaving screen Pause work, save draft state
viewDidDisappear After leaving screen Stop expensive work, cancel tasks
viewDidLayoutSubviews After layout updates frames Frame-dependent work only

Example:

swift
override func viewDidLoad() {
    super.viewDidLoad()
    setupViews()
    setupConstraints()
    bindViewModel()
}

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    refreshIfNeeded()
}

override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)
    task?.cancel()
}

Common mistakes:

  • Doing heavy synchronous work in viewDidLoad
  • Starting network work without cancellation
  • Adding constraints repeatedly in viewDidLayoutSubviews
  • Assuming viewDidLoad runs every time the screen appears
  • Updating UI from a background thread
  • Not handling repeated appearances after tab switch or navigation pop

Where to put common work:

Work Better place
One-time UI setup viewDidLoad
Navigation bar appearance viewWillAppear
Analytics screen event viewDidAppear
Cancel async task viewWillDisappear or viewDidDisappear
Frame-based corner radius/shadow path viewDidLayoutSubviews
Data refresh every time screen opens viewWillAppear or ViewModel trigger

Senior-level topics:

  • State restoration
  • Memory pressure handling
  • Task cancellation when screen disappears
  • Avoiding retain cycles from closures started in lifecycle methods
  • Difference between view loading and view appearing

A strong answer is:

“I use viewDidLoad for one-time setup, appearance callbacks for refresh and analytics, viewDidLayoutSubviews only for frame-dependent layout work, and disappearance callbacks to pause or cancel work.”

How does UITableView reuse work?

UITableView reuses cells for performance. Instead of creating a new cell for every row, the table view keeps a reuse pool and gives you a recycled cell when needed.

swift
let cell = tableView.dequeueReusableCell(
    withIdentifier: "ItemCell",
    for: indexPath
) as! ItemCell

cell.configure(with: items[indexPath.row])
return cell

Why reuse matters:

  • Smooth scrolling
  • Lower memory use
  • Fewer view allocations
  • Better battery/performance

Important rule: always configure the full cell state.

A reused cell may still contain old state from a previous row, so reset values such as:

  • Image
  • Text
  • Selection state
  • Loading indicator
  • Hidden labels
  • Accessory type
  • Async image task

Example:

swift
final class ItemCell: UITableViewCell {
    override func prepareForReuse() {
        super.prepareForReuse()
        titleLabel.text = nil
        thumbnailView.image = nil
        imageTask?.cancel()
        imageTask = nil
    }
}

Modern table updates often use diffable data sources instead of manually calling many insert/delete/reload methods.

swift
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.main])
snapshot.appendItems(items)
dataSource.apply(snapshot, animatingDifferences: true)

Diffable data sources help avoid common update crashes like:

text
Invalid update: invalid number of rows in section

For image-heavy lists, also know prefetching:

  • UITableViewDataSourcePrefetching
  • Start image/data loading before row appears
  • Cancel prefetch when row is no longer likely to appear
  • Avoid updating a reused cell with an old async result

Common async image bug:

swift
// Bad if cell is reused before image returns
cell.imageView.image = downloadedImage

Better: verify the cell still represents the same model before applying the image.

A strong answer is:

“Table views reuse cells to keep scrolling fast. I fully configure cells every time, reset state in prepareForReuse, cancel old async work, and use diffable data sources for safer list updates.”

Auto Layout — intrinsic content size and constraints?

Auto Layout calculates view positions and sizes from constraints.

Important concepts:

Concept Meaning
Constraint Relationship between views or anchors
Intrinsic content size Natural size of a view, such as label text size
Content hugging How strongly a view resists growing
Compression resistance How strongly a view resists shrinking
Safe area Area not covered by notch, home indicator, bars
Priority Strength of a constraint
Ambiguous layout Not enough constraints to determine layout
Unsatisfiable layout Conflicting constraints

Example programmatic constraints:

swift
NSLayoutConstraint.activate([
    titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16),
    titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
    titleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16)
])

For Auto Layout in code, remember:

swift
titleLabel.translatesAutoresizingMaskIntoConstraints = false

Intrinsic content size example:

  • A UILabel knows its natural size from its text and font
  • A UIButton knows its natural size from title and content insets
  • An image view may need explicit width/height if image size is not enough

Content hugging vs compression resistance:

Priority Question it answers
Content hugging Which view should avoid growing?
Compression resistance Which view should avoid shrinking?

Common interview debugging points:

  • Check console for constraint conflict logs
  • Use Debug View Hierarchy
  • Add identifiers to constraints
  • Look for missing width/height or edge constraints
  • Avoid adding duplicate constraints repeatedly
  • Use safe area instead of hardcoding top/bottom offsets

Do not create constraints repeatedly inside viewDidLayoutSubviews. That method can run many times.

Use viewDidLayoutSubviews only when actual frames are needed, such as:

  • Updating shadow path
  • Applying frame-based gradients
  • Adjusting rounded corners based on final size

A strong answer is:

“Auto Layout solves constraints to determine size and position. I use intrinsic content size, hugging, compression resistance, priorities, and safe areas to build adaptive layouts, and I debug conflicts with the console and View Hierarchy.”

Storyboards vs programmatic UI — trade-offs?

Storyboards and programmatic UI are both valid. A good interview answer should discuss team workflow, not personal religion.

Storyboards Programmatic UI
Visual layout Code-reviewed layout
Fast for simple screens Better for reusable components
Useful for small teams/prototypes Better for large teams and Git merges
Segues visible Navigation is explicit in code
Merge conflicts can be painful More verbose
Harder to refactor at scale Easier to search and modularize

Storyboards are useful when:

  • The team is comfortable with Interface Builder
  • Screens are simple
  • Designers/developers benefit from visual layout
  • Legacy UIKit app already uses storyboards

Programmatic UI is useful when:

  • Many developers work on the same screens
  • You need reusable components
  • You want easier code review
  • You want fewer merge conflicts
  • UI is built dynamically
  • Architecture uses coordinators/factories

SwiftUI changes the discussion because it is declarative and code-first:

swift
struct ProfileView: View {
    var body: some View {
        VStack {
            Text("Profile")
            Button("Edit") {}
        }
    }
}

Many modern apps mix approaches:

  • UIKit legacy screens
  • SwiftUI for new features
  • UIHostingController to embed SwiftUI inside UIKit
  • UIViewRepresentable to use UIKit views inside SwiftUI

A strong senior answer should include migration reality. Most teams do not rewrite a stable storyboard app just because programmatic UI is fashionable.

A strong answer is:

“Storyboards are good for visual simple flows, while programmatic UI is better for large teams, reusable components, and code review. I choose based on team workflow, app size, and existing codebase—not ideology.”


SwiftUI

SwiftUI vs UIKit — when would you choose each in 2026?

SwiftUI and UIKit can coexist in the same iOS app.

SwiftUI UIKit
Declarative UI Imperative UI
Fast for new screens Mature and battle-tested
Great with state-driven UI Fine-grained control
Less boilerplate More APIs and legacy support
Strong for forms/settings/lists Strong for complex custom interactions
Natural with Observation/concurrency Common in older enterprise apps

Choose SwiftUI for:

  • New greenfield screens
  • Forms, settings, dashboards, lists
  • State-driven UI
  • Faster iteration
  • Multi-platform UI where practical
  • Teams already targeting modern iOS versions

Choose UIKit for:

  • Existing UIKit apps
  • Complex custom transitions
  • Heavy collection/table customization
  • Mature third-party UIKit SDKs
  • Features where SwiftUI API support is still limiting
  • Very fine-grained layout/gesture control

Interop is important:

swift
// Embed SwiftUI in UIKit
let controller = UIHostingController(rootView: ProfileView())
swift
// Wrap UIKit for SwiftUI
struct CameraPreview: UIViewRepresentable {
    func makeUIView(context: Context) -> UIView { UIView() }
    func updateUIView(_ uiView: UIView, context: Context) {}
}

A practical answer: use SwiftUI for new features when deployment target and team skill allow it, but keep UIKit where the existing app or required behavior makes it the safer choice.

A strong answer is:

“I prefer SwiftUI for new state-driven screens, but I still use UIKit for legacy apps, complex custom UI, or APIs where UIKit gives better control. In real apps, I often mix both using UIHostingController and representable wrappers.”

@State vs @StateObject vs @ObservedObject vs @EnvironmentObject?

SwiftUI property wrappers define who owns the state and how changes update the view.

Wrapper Owns data? Best use
@State View owns simple local state Toggle, text input, selected tab
@Binding Parent owns state Child edits parent state
@StateObject View creates and owns ObservableObject Legacy ViewModel lifecycle
@ObservedObject External owner provides object Passed-in legacy ViewModel
@EnvironmentObject Ancestor injects shared object App/session-wide shared state
@Environment Reads environment value Theme, locale, dependencies

Example local state:

swift
@State private var isExpanded = false

Legacy ObservableObject ownership:

swift
@StateObject private var viewModel = ProfileViewModel()

Passed-in object:

swift
@ObservedObject var viewModel: ProfileViewModel

Common bug:

swift
// Bad: new object can be recreated with view updates
@ObservedObject var viewModel = ProfileViewModel()

If the view creates the object, use @StateObject in the older ObservableObject model.

Modern Observation note:

With the newer @Observable macro, many simple models can be owned with @State instead of @StateObject.

swift
@Observable
final class ProfileModel {
    var name = ""
}

struct ProfileView: View {
    @State private var model = ProfileModel()

    var body: some View {
        Text(model.name)
    }
}

Use @EnvironmentObject carefully. It is convenient, but hidden dependencies can make previews and tests fail if the object is not injected.

A strong answer is:

“The key is ownership. @State is local value state, @Binding edits parent state, @StateObject owns a legacy observable object, @ObservedObject observes an externally owned object, and @EnvironmentObject is shared from an ancestor.”

Why are SwiftUI Views structs?

SwiftUI views are structs because they are lightweight descriptions of UI for a given state.

They are not long-lived view objects like UIView.

swift
struct ProfileView: View {
    let name: String

    var body: some View {
        Text(name)
    }
}

When state changes, SwiftUI can recreate the view value and compare/update the underlying rendered UI efficiently.

Important interview points:

  • body can be evaluated many times
  • Views should be cheap to create
  • Do not store long-lived business state directly in a view struct
  • Avoid expensive work inside body
  • Put async work in .task, services, or ViewModels
  • Use stable identity in lists

Bad pattern:

swift
var body: some View {
    Text(expensiveCalculation())
}

Better:

  • Precompute in model/ViewModel
  • Use cached state
  • Use @State, @Observable, or a ViewModel
  • Extract subviews for clarity
  • Use .task for async loading

View identity matters:

swift
ForEach(users, id: \.id) { user in
    UserRow(user: user)
}

Avoid unstable IDs like UUID() generated inside body, because they make SwiftUI think every item is new.

A strong answer is:

“SwiftUI views are structs because they are cheap descriptions of UI, not persistent UI objects. Persistent state belongs in state properties, models, or ViewModels, and body should stay lightweight.”

How do you handle navigation in SwiftUI?

Modern SwiftUI navigation uses NavigationStack.

Basic value-based navigation:

swift
NavigationStack {
    List(items) { item in
        NavigationLink(value: item) {
            Text(item.title)
        }
    }
    .navigationDestination(for: Item.self) { item in
        DetailView(item: item)
    }
}

For programmatic navigation, use a path.

swift
@State private var path: [Route] = []

var body: some View {
    NavigationStack(path: $path) {
        HomeView()
            .navigationDestination(for: Route.self) { route in
                switch route {
                case .profile(let id):
                    ProfileView(id: id)
                case .settings:
                    SettingsView()
                }
            }
    }
}

Route enum:

swift
enum Route: Hashable {
    case profile(User.ID)
    case settings
}

This gives you:

  • Programmatic push
  • Programmatic pop
  • Testable route state
  • Deep link mapping
  • Clear navigation destinations

Common patterns:

Need SwiftUI approach
Push detail screen NavigationLink(value:)
Programmatic push Append to path
Pop one screen Remove last path item
Pop to root Clear path
Deep link Convert URL to route path
Split iPad/mac layout NavigationSplitView

UIKit equivalent is UINavigationController with push/pop.

For mixed apps, you may use SwiftUI inside a UIKit navigation controller or embed UIKit screens from SwiftUI where needed.

A strong answer is:

“I use NavigationStack with value-based routes and navigationDestination. For larger apps, I model routes with an enum and keep navigation path in a router so push, pop, and deep links are testable.”

SwiftUI performance pitfalls?

SwiftUI performance issues often come from unstable identity, excessive observation, heavy body work, or too much work on the main actor.

Pitfall Better approach
Expensive work in body Move to model/ViewModel, cache, or compute once
Unstable IDs in ForEach Use stable model IDs
One huge view Extract smaller views
Over-observation Split observable state by feature/screen
Network/image work on main thread Use async APIs and update UI on main actor
Too many updates from parent state Move state closer to where it is used
Large lists without lazy containers Use List, LazyVStack, pagination
Recreating objects in body Own them with the right state wrapper

Bad identity example:

swift
ForEach(items, id: \.self) { item in
    Row(item: item)
}

This can be okay for simple stable values, but for models, prefer stable IDs.

swift
ForEach(items, id: \.id) { item in
    Row(item: item)
}

Bad body example:

swift
var body: some View {
    List(viewModel.sortAndFilter(items)) { item in
        Row(item: item)
    }
}

Better:

  • Store sorted/filtered data in model state
  • Use a computed value only if it is cheap
  • Recalculate when inputs change, not every render
  • Use pagination for large data sets

Useful tools:

  • Instruments
  • SwiftUI performance/hitches profiling
  • Time Profiler
  • Memory Graph
  • OSLog/signposts for expensive flows

Senior-level point: performance is often about state design. If one global object changes and many views observe it, SwiftUI may update more UI than necessary.

A strong answer is:

“I keep SwiftUI views lightweight, use stable identity, avoid expensive body work, split observable state, and profile with Instruments instead of guessing.”


Concurrency and threading

GCD vs async/await — what do you say in 2026?

GCD and Swift structured concurrency both handle async work, but they have different programming models.

GCD async/await
Queue-based Task-based
Callback style Linear async code
Manual dispatch to queues Suspension with await
Harder cancellation flow Structured cancellation support
Easy to create callback pyramids Easier error propagation
Legacy and still common Preferred for modern Swift

GCD example:

swift
DispatchQueue.global().async {
    let data = loadData()

    DispatchQueue.main.async {
        self.updateUI(data)
    }
}

Modern async/await:

swift
func loadFeed() async throws -> [Post] {
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode([Post].self, from: data)
}

Calling from UI:

swift
Task {
    do {
        posts = try await service.loadFeed()
    } catch {
        errorMessage = error.localizedDescription
    }
}

Still know GCD because many older apps and SDKs use:

  • DispatchQueue.main.async
  • Completion handlers
  • Operation queues
  • Legacy callback APIs

Modern answer should include:

  • Use async/await for new code
  • Use actors for shared mutable state
  • Use @MainActor for UI state
  • Use continuations to bridge callback APIs
  • Use GCD when working with legacy code or low-level queue control

A strong answer is:

“I prefer async/await for new Swift code because it gives clearer control flow, error handling, and cancellation. I still understand GCD because older iOS code and some APIs use queues and callbacks.”

What is @MainActor and why does it matter?

@MainActor is a global actor used to isolate code that must run on the main actor, which is where UI updates should happen.

swift
@MainActor
final class FeedViewModel: ObservableObject {
    @Published var posts: [Post] = []
    @Published var isLoading = false

    func load() async {
        isLoading = true
        defer { isLoading = false }

        do {
            posts = try await service.fetchPosts()
        } catch {
            posts = []
        }
    }
}

Why it matters:

  • UI state should be updated on the main actor
  • Prevents accidental background UI updates
  • Replaces scattered DispatchQueue.main.async
  • Makes thread expectations visible in the type system
  • Helps with Swift concurrency safety

You can mark:

  • A class
  • A method
  • A property
  • A closure
  • A ViewModel that owns UI state

Example method-only annotation:

swift
@MainActor
func apply(_ posts: [Post]) {
    self.posts = posts
}

Common mistake:

swift
Task.detached {
    self.posts = posts // unsafe for UI state
}

Better:

swift
Task {
    let posts = try await service.fetchPosts()
    await MainActor.run {
        self.posts = posts
    }
}

If the whole ViewModel is @MainActor, you usually do not need scattered MainActor.run calls for its properties.

Senior nuance: do not put heavy CPU work on the main actor. Do background work outside, then return to the main actor only to update UI state.

A strong answer is:

@MainActor isolates UI-related code to the main actor. I use it for ViewModels and UI state so updates are safe and explicit, while keeping heavy work off the main actor.”

What is an actor in Swift?

An actor is a reference type that protects its mutable state from data races.

Only one task at a time can access actor-isolated mutable state.

swift
actor ImageCache {
    private var storage: [URL: Data] = [:]

    func data(for url: URL) -> Data? {
        storage[url]
    }

    func insert(_ data: Data, for url: URL) {
        storage[url] = data
    }
}

Using the actor:

swift
let cache = ImageCache()

await cache.insert(data, for: url)
let cached = await cache.data(for: url)

Why actors matter:

  • Safer shared mutable state
  • Compiler-enforced isolation
  • Less manual locking
  • Better fit with async/await
  • Helpful for caches, managers, and background state

Actor vs class with lock:

Class + lock Actor
Manual locking Compiler-enforced isolation
Easy to forget lock Access requires await from outside
Risk deadlocks Safer state access
More boilerplate Clear concurrency model

Important nuance: actors are reentrant. When an actor method hits an await, other work may run on that actor before the original method resumes.

swift
actor BankAccount {
    var balance = 100

    func withdraw(_ amount: Int) async -> Bool {
        guard balance >= amount else { return false }
        await logWithdrawal(amount)
        balance -= amount
        return true
    }
}

The await can allow interleaving. For critical state, update state before suspension or re-check after suspension.

A strong answer is:

“An actor is a reference type that serializes access to its isolated state. I use actors for shared mutable state instead of manually locking, but I remember that actor methods can be reentrant across await points.”

What is Sendable and why does Swift 6 matter?

Sendable marks values that are safe to pass across concurrency boundaries, such as tasks and actors.

A type is sendable when it can be safely used from concurrent code without causing data races.

Examples usually safe:

  • Immutable value types
  • Structs whose stored properties are also Sendable
  • Enums with sendable associated values
  • Actors
  • Carefully designed final classes

Example:

swift
struct User: Sendable {
    let id: UUID
    let name: String
}

Classes need more care because they can hold shared mutable state.

swift
final class Counter: @unchecked Sendable {
    private let lock = NSLock()
    private var value = 0
}

@unchecked Sendable means you promise the compiler that the type is safe. Use it sparingly because the compiler will not fully verify your claim.

Why Swift 6 matters:

Swift 6 language mode makes concurrency checking stricter. Code that only produced warnings in earlier modes may become errors when unsafe sharing crosses actor/task boundaries.

Common migration fixes:

  • Add @MainActor to UI-facing types
  • Make DTOs and value models Sendable
  • Avoid capturing mutable non-sendable state in concurrent closures
  • Replace shared mutable classes with actors
  • Use nonisolated carefully for safe actor members
  • Bridge legacy callbacks with continuations
  • Enable strict checking per module instead of fixing everything at once

Common interview scenario:

“We turned on stricter concurrency checks and many warnings appeared around ViewModels, delegates, and shared services. I fixed UI state with @MainActor, converted simple models to Sendable, and moved shared mutable caches behind actors.”

A strong answer is:

Sendable tells Swift a value can safely cross concurrency boundaries. Swift 6 matters because stricter concurrency checking forces unsafe shared mutable state to be fixed instead of silently becoming a data race.”

Task, async let, and TaskGroup?

Swift gives different tools for different async needs.

Tool Use
Task { } Start async work from synchronous context
Task.detached { } Unstructured work not inheriting current actor/task context
async let Fixed number of parallel child tasks
withTaskGroup Dynamic number of child tasks
withThrowingTaskGroup Dynamic child tasks that can throw

Task example:

swift
Task {
    await viewModel.load()
}

async let example for a fixed number of parallel calls:

swift
async let users = fetchUsers()
async let posts = fetchPosts()

let feed = try await Feed(users: users, posts: posts)

Task group for dynamic parallel work:

swift
let images = await withTaskGroup(of: UIImage?.self) { group in
    for url in urls {
        group.addTask {
            await loadImage(url)
        }
    }

    var result: [UIImage] = []

    for await image in group {
        if let image {
            result.append(image)
        }
    }

    return result
}

Structured concurrency means child tasks are tied to the parent scope. This helps with:

  • Cancellation
  • Error propagation
  • Avoiding orphan tasks
  • Reasoning about task lifetime

Important caution:

Task.detached does not inherit the same actor context or priority in the same way. Avoid it for normal UI work unless you know why you need it.

Screen-level example:

  • Start load in .task or Task
  • If the screen disappears, task can be cancelled
  • Check cancellation in long-running work
  • Do not update UI after cancellation

A strong answer is:

“I use Task to start async work, async let for a fixed number of parallel operations, and task groups for dynamic parallel work. I prefer structured concurrency because child task lifetime and cancellation are easier to reason about.”

What are checked continuations?

Checked continuations bridge callback-based APIs into async/await.

Legacy callback API:

swift
func fetchLegacy(completion: @escaping (Result<Data, Error>) -> Void) {
    // older SDK callback
}

Async wrapper:

swift
func fetchLegacyAsync() async throws -> Data {
    try await withCheckedThrowingContinuation { continuation in
        fetchLegacy { result in
            continuation.resume(with: result)
        }
    }
}

Use:

API Use
withCheckedContinuation Callback returns success value only
withCheckedThrowingContinuation Callback can return error
withUnsafeContinuation Lower-level, fewer runtime checks

Why “checked” matters:

  • Helps detect double resume bugs
  • Helps detect paths where continuation is never resumed
  • Safer for migrations from callback code

Common mistakes:

swift
// Bug: continuation may resume twice
completionA = {
    continuation.resume(returning: data)
}

completionB = {
    continuation.resume(returning: otherData)
}

A continuation must be resumed exactly once.

Also handle cancellation if the underlying API supports it. For example, if you wrap a network task, store the task and cancel it when the Swift task is cancelled.

Good use cases:

  • Legacy SDK callbacks
  • Delegate/callback APIs
  • Old URLSession completion handlers
  • One-shot async operations

Bad use cases:

  • Multi-event streams
  • Repeated delegate events
  • Notifications over time

For streams of many values, consider AsyncStream or AsyncThrowingStream instead of a single continuation.

A strong answer is:

“Checked continuations let me wrap a one-shot callback API in async/await. I must resume the continuation exactly once, use the throwing version for errors, and consider cancellation when the underlying API supports it.”


Networking, persistence, and security

How do you make a network request on iOS?

Modern iOS networking usually uses URLSession with async/await.

For a simple GET request:

swift
struct APIClient {
    func get<T: Decodable>(_ type: T.Type, from url: URL) async throws -> T {
        let (data, response) = try await URLSession.shared.data(from: url)

        guard let http = response as? HTTPURLResponse else {
            throw APIError.invalidResponse
        }

        guard (200...299).contains(http.statusCode) else {
            throw APIError.badStatus(http.statusCode)
        }

        return try JSONDecoder().decode(T.self, from: data)
    }
}

For real apps, prefer URLRequest when you need headers, method, body, cache policy, or timeout.

swift
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.timeoutInterval = 30
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")

let (data, response) = try await URLSession.shared.data(for: request)

A production-ready answer should discuss:

Topic What to say
Status code Check HTTPURLResponse and handle non-2xx
Decoding Decode DTOs with JSONDecoder
Errors Separate network, status, decoding, and auth errors
Timeout Configure request/session timeout
Retry Retry safe transient failures, usually GET
Auth Add token through request builder or API client
Cancellation Cancel task when screen disappears
Caching Use URLCache or app-level cache where useful
Background transfer Use background URLSessionConfiguration for large uploads/downloads
Certificate pinning Advanced security; use only when operationally justified

Avoid blindly retrying unsafe requests such as payment or order creation unless the backend supports idempotency keys.

Example error enum:

swift
enum APIError: Error {
    case invalidResponse
    case badStatus(Int)
    case decoding(Error)
}

Senior-level answer: keep networking out of ViewControllers. Put it behind an API client/repository protocol so ViewModels can be tested with fakes.

A strong answer is:

“I use URLSession with async/await, validate the HTTP response, decode into DTOs, and expose typed errors. In production, I also handle timeouts, cancellation, auth headers, retry policy, caching, and testability through an injected API client.”

Core Data vs SwiftData vs UserDefaults?

Choose persistence based on data size, structure, security, and deployment target.

Store Best use
UserDefaults Small preferences, feature flags, simple settings
Keychain Passwords, tokens, credentials, sensitive secrets
Files Images, documents, downloaded blobs
SQLite Custom relational storage when you need control
Core Data Mature object graph, complex queries, migrations, older iOS support
SwiftData Swift-native model persistence, SwiftUI-friendly, iOS 17+

Use UserDefaults for small non-sensitive values:

swift
UserDefaults.standard.set(true, forKey: "hasSeenOnboarding")

Do not store secrets in UserDefaults.

Use Keychain for:

  • Access tokens
  • Refresh tokens
  • Passwords
  • Private credentials
  • Sensitive account identifiers

Core Data is still important because many existing enterprise apps use it. It is mature, powerful, and supports complex persistence needs.

SwiftData is newer and more Swift-friendly:

swift
@Model
final class Article {
    var title: String
    var isBookmarked: Bool

    init(title: String, isBookmarked: Bool = false) {
        self.title = title
        self.isBookmarked = isBookmarked
    }
}

SwiftData works nicely with SwiftUI, but deployment target matters. If your app supports older iOS versions, Core Data may still be the practical choice.

Offline-first interview answer:

  • Local database is the source of truth
  • UI reads from local store
  • Sync service updates local store
  • Network failure should not break reading
  • Conflicts need a clear policy
  • Migrations must be planned before schema changes

Senior topics:

  • Lightweight/heavyweight migrations
  • Conflict resolution
  • Batch deletes
  • Background contexts
  • Sync status fields
  • DTO-to-persistent-model mapping
  • Core Data NSFetchedResultsController
  • SwiftData @Query

A strong answer is:

“I use UserDefaults only for small non-sensitive preferences, Keychain for secrets, Core Data for mature complex persistence or older iOS support, and SwiftData for modern SwiftUI-friendly persistence when the deployment target allows it.”

App Transport Security and Keychain — interview basics?

iOS security interview answers should show defense in depth, not one magic API.

Topic Interview answer
ATS Enforces safer network transport, HTTPS by default
Keychain Stores credentials/secrets securely
LocalAuthentication Verifies user presence with Face ID/Touch ID/passcode
Privacy manifests Declare data use and required-reason APIs
ATT Required for tracking users across apps/sites
Certificate pinning Optional advanced protection with operational cost
Route/UI locks Convenience only; server must enforce real authorization

App Transport Security (ATS) helps prevent insecure network connections. Avoid broad exceptions like allowing all HTTP traffic.

Bad pattern:

xml
<key>NSAllowsArbitraryLoads</key>
<true/>

If an exception is needed, make it narrow and justify it.

Keychain is for sensitive data:

  • Access token
  • Refresh token
  • Password
  • Private credential
  • API secret issued to the user/device

Do not store secrets in:

  • UserDefaults
  • Plain plist
  • Local JSON file
  • Hardcoded constants

LocalAuthentication is commonly used for Face ID / Touch ID / passcode prompts. It proves user presence, but it does not replace backend authorization or secure token handling.

Example interview distinction:

“Face ID can unlock access to a token stored securely, but the server still decides whether the request is authorized.”

Privacy topics to mention in modern interviews:

  • Ask only for permissions you need
  • Explain permission purpose clearly
  • Keep privacy manifest accurate
  • Be careful with third-party SDKs
  • Do not collect tracking data without proper consent
  • Avoid using required-reason APIs without declaring valid reasons

A strong answer is:

“I rely on ATS for secure transport, Keychain for secrets, LocalAuthentication for user presence, and backend authorization for real access control. I also keep privacy manifests and permission usage aligned with what the app actually does.”


Testing, release, and scenarios

What do interviewers expect from XCTest?

Interviewers expect you to write testable iOS code, not only know XCTest syntax.

Common test layers:

Test type What to test
Unit tests ViewModels, services, parsers, validators, pure Swift logic
Async tests async/await code, repositories, use cases
UI tests Critical user flows like login, checkout, onboarding
Snapshot tests Visual regression, if the team uses a library
Integration tests Feature behavior with multiple real components

Simple decoding test:

swift
func testDecodeUser() throws {
    let json = #"{"id":"1","name":"Ada"}"#.data(using: .utf8)!

    let user = try JSONDecoder().decode(User.self, from: json)

    XCTAssertEqual(user.name, "Ada")
}

Async XCTest example:

swift
func testFetchUsers() async throws {
    let repository = MockUserRepository()

    let users = try await repository.fetchUsers()

    XCTAssertEqual(users.count, 1)
}

Good testing practices:

  • Inject dependencies through protocols
  • Do not hit real network in unit tests
  • Use fake repositories for ViewModels
  • Use custom URLProtocol or mock API client for networking
  • Test loading, success, empty, and failure states
  • Keep UI tests fewer and focused because they are slower
  • Avoid testing implementation details

Example ViewModel test idea:

swift
func testLoadShowsUsers() async {
    let viewModel = UserListViewModel(repository: MockUserRepository())

    await viewModel.load()

    XCTAssertEqual(viewModel.state, .loaded([User(id: "1", name: "Ada")]))
}

Modern note: Apple also has the newer Swift Testing framework, but XCTest is still widely used in existing iOS projects and interviews.

A strong answer is:

“I unit test ViewModels and pure logic with injected dependencies, test async code using async XCTest methods, avoid real network in unit tests, and reserve UI tests for the most important flows.”

What is code signing and why do interviewers ask?

Code signing proves the app comes from a trusted developer and has the entitlements it claims.

Interviewers ask because it shows whether you have shipped real apps, not only run code on the Simulator.

Important terms:

Term Meaning
Bundle ID Unique app identifier
App ID Apple Developer identity for an app/capability set
Certificate Proves developer/team identity
Provisioning profile Connects app ID, certificate, devices, and entitlements
Entitlements Permissions/capabilities granted to the app
Development signing Run on local devices during development
Distribution signing TestFlight/App Store/enterprise distribution
Capabilities Push, App Groups, Keychain Sharing, iCloud, Sign in with Apple

Provisioning profiles matter because they define what the signed app is allowed to do.

Examples of capabilities:

  • Push notifications
  • App Groups
  • Keychain sharing
  • Associated domains
  • iCloud/CloudKit
  • Background modes

Common problems:

  • Wrong bundle ID
  • Expired certificate
  • Missing device in development profile
  • Capability enabled in code but not in profile
  • App Group mismatch
  • Push notification entitlement missing
  • CI machine missing signing credentials

CI/CD signing topics:

  • Xcode automatic signing
  • Xcode Cloud
  • fastlane match
  • Secure certificate/profile storage
  • Separate development, staging, and production bundle IDs
  • TestFlight distribution

Good interview answer: code signing is not only paperwork. It affects whether push, Keychain sharing, associated domains, App Groups, and app distribution work.

A strong answer is:

“Code signing proves app identity and controls entitlements. Provisioning profiles connect the bundle ID, certificate, devices, and capabilities. Interviewers ask because signing issues appear in real device testing, CI, TestFlight, and App Store releases.”

Scenario: Design offline-first reading list app.

An offline-first reading list should treat local storage as the source of truth.

High-level design:

  1. Store articles locally using Core Data or SwiftData
  2. Render UI from the local store
  3. Sync with server on launch, foreground, and background refresh
  4. Queue local changes when offline
  5. Retry sync when network returns
  6. Resolve conflicts with a clear policy
  7. Cache images separately
  8. Show sync/offline state to the user

Suggested model fields:

swift
Article {
    id
    title
    body
    updatedAt
    isBookmarked
    syncStatus
    serverVersion
}

UI behavior:

  • User can read saved articles offline
  • Bookmark action updates local DB immediately
  • UI shows optimistic update
  • Sync service uploads pending changes later
  • Offline banner appears when network is unavailable
  • Failed sync does not lose user actions

Sync policy:

Problem Possible solution
Same article edited on two devices Server version or updated timestamp
Bookmark changed offline Queue local operation
Delete vs update conflict Define server/client precedence
Large images Disk cache with eviction
Background limits Use background tasks carefully

Use BGAppRefreshTask for periodic refresh, but do not promise exact timing. iOS gives limited background execution based on system conditions.

Testing plan:

  • Airplane mode
  • Slow/flaky network
  • App kill and relaunch
  • Conflict simulation
  • Partial sync failure
  • Large article list
  • Background refresh behavior

Senior-level point: offline-first is mostly a data consistency problem, not only a local database problem.

A strong answer is:

“I would make the local database the source of truth, update it optimistically, sync deltas with the server, queue offline changes, handle conflicts explicitly, and test airplane mode, flaky network, and app relaunch scenarios.”

Scenario: UITableView scrolls poorly with remote images.

Poor scrolling with remote images usually comes from doing too much work on the main thread or not handling cell reuse correctly.

Diagnosis path:

  1. Confirm cells are reused correctly
  2. Reset image state in prepareForReuse
  3. Cancel old image requests when a cell is reused
  4. Avoid decoding huge images on the main thread
  5. Downsample images to cell size
  6. Add memory cache
  7. Add disk cache if images repeat
  8. Use prefetching for upcoming rows
  9. Verify with Instruments

Cell reuse fix:

swift
override func prepareForReuse() {
    super.prepareForReuse()
    thumbnailView.image = placeholderImage
    imageTask?.cancel()
    imageTask = nil
}

Async loading safety:

swift
cell.configure(title: item.title, image: placeholderImage)

imageLoader.load(url: item.imageURL) { [weak tableView] image in
    guard let currentCell = tableView?.cellForRow(at: indexPath) as? ItemCell else {
        return
    }

    currentCell.thumbnailView.image = image
}

In production, also verify that the cell still represents the same item. Index paths can change when the table updates.

Performance fixes:

Problem Fix
Full-size 4K image decoded for small cell Downsample to target size
Network request repeats while scrolling Memory/disk cache
Old image appears in reused cell Reset and verify identity
Scroll hitch Move decode work off main thread
Late image updates wrong cell Cancel task and check model ID
Loading starts too late UITableViewDataSourcePrefetching

SwiftUI note: AsyncImage is convenient, but production apps often need a custom image loader for caching, cancellation, placeholders, retries, and downsampling.

Measure with:

  • Time Profiler
  • Allocations
  • Memory Graph
  • Network instrument
  • Core Animation hitches

A strong answer is:

“I would check reuse first, reset and cancel work in prepareForReuse, downsample images off the main thread, add cache layers, use prefetching, and verify smooth scrolling with Instruments.”

Scenario: Modularize large iOS app?

A large iOS app should be modularized around features, shared foundations, and team boundaries.

Common module layout:

text
App
Features/
  FeedFeature
  ProfileFeature
  SettingsFeature
Core/
  Networking
  Persistence
  Analytics
  DesignSystem
  Utilities

Common tools:

  • Swift Package Manager
  • Xcode projects/workspaces
  • Tuist or similar project generation tools
  • Internal frameworks/packages

Good module types:

Module Purpose
App Composition root, app lifecycle, dependency wiring
Feature Screen/flow-specific code
DesignSystem Reusable UI components, colors, typography
Networking API client, request building, auth transport
Persistence Core Data/SwiftData wrapper, storage abstractions
Analytics Event tracking abstraction
Domain/Core Shared models and business rules
TestingSupport Mocks, fakes, test helpers

Benefits:

  • Faster focused builds
  • Clear ownership
  • Better testability
  • Safer refactoring
  • Reduced merge conflicts
  • Reusable features/components
  • Cleaner dependency direction

Boundary rules:

  • Feature modules should not depend on each other directly
  • Shared modules should stay generic
  • UI modules should not know backend details
  • Public APIs should be intentionally small
  • App module wires dependencies together
  • Avoid circular dependencies

Example dependency direction:

text
App
 → Feature
   → DesignSystem
   → Domain
   → Networking protocol

Avoid premature micro-modules. Too many tiny modules increase build complexity and developer friction.

Split when:

  • Multiple teams own different areas
  • Build time is too slow
  • Feature boundaries are stable
  • Code ownership is unclear
  • Reuse is real, not imagined
  • App also ships SDK/internal packages

Senior-level topics:

  • Public API stability
  • Deprecation policy
  • Dependency inversion
  • CI build caching
  • Binary vs source packages
  • Feature flags
  • Release cadence
  • Test ownership per module

A strong answer is:

“I modularize by feature and shared capability, enforce dependency direction, keep public APIs small, and avoid premature micro-modules. The goal is faster builds, clearer ownership, and safer team scaling.”

How do you approach accessibility on iOS?

Accessibility should be designed into the screen, not added at the end.

Important areas:

Area What to do
VoiceOver Labels, hints, traits, reading order
Dynamic Type Scalable fonts and flexible layouts
Contrast Text and controls readable in all themes
Color Do not use color as the only signal
Touch targets Make controls easy to tap
Motion Respect Reduce Motion
Custom controls Add correct accessibility role/traits
Forms Clear labels, errors, and focus behavior
Testing VoiceOver, Accessibility Inspector, largest text sizes

UIKit example:

swift
button.accessibilityLabel = "Add article"
button.accessibilityHint = "Adds this article to your reading list"
button.accessibilityTraits = [.button]

SwiftUI example:

swift
Image(systemName: "bookmark")
    .accessibilityLabel("Bookmark article")

Dynamic Type:

  • Use system text styles where possible
  • Avoid fixed-height labels
  • Test the largest accessibility text sizes
  • Let layouts wrap or stack vertically
  • Avoid clipped text

VoiceOver:

  • Ensure custom controls have meaningful labels
  • Group related elements when it improves navigation
  • Keep reading order logical
  • Do not expose decorative images
  • Announce important dynamic updates when needed

Common mistakes:

  • Clickable image with no label
  • Custom button built from View but no trait
  • Text clipped at large sizes
  • Error message shown visually but not accessible
  • Color-only validation state
  • Modal opens without focus management

Senior answer: accessibility improves quality for everyone and is often required in enterprise, government, healthcare, finance, and public-facing apps.

A strong answer is:

“I support VoiceOver, Dynamic Type, contrast, touch targets, and Reduce Motion from the start. I test with real accessibility settings and make custom controls expose the right labels, hints, traits, and focus behavior.”

Final-week checklist for iOS developer interviews?

Use the final week to revise practical explanations and scenarios.

Swift fundamentals:

  • Struct vs class
  • Value vs reference semantics
  • Optionals, guard let, and force unwrap risk
  • Enums with associated values
  • Protocol-oriented programming
  • Generics, some, and any
  • Codable pitfalls
  • Copy-on-write

Memory and architecture:

  • ARC and retain cycles
  • weak vs unowned
  • Closure capture lists
  • MVC vs MVVM
  • Delegate pattern
  • NotificationCenter trade-offs
  • Dependency injection
  • Coordinator pattern

UIKit and SwiftUI:

  • View controller lifecycle
  • UITableView reuse and diffable data source
  • Auto Layout basics
  • Storyboard vs programmatic UI
  • SwiftUI vs UIKit
  • SwiftUI state wrappers
  • Why SwiftUI views are structs
  • NavigationStack
  • SwiftUI performance pitfalls

Concurrency:

  • GCD vs async/await
  • @MainActor
  • Actors
  • Sendable
  • Task, async let, and task groups
  • Checked continuations
  • Cancellation

Networking, persistence, release:

  • URLSession request flow
  • HTTP status and decoding errors
  • Core Data vs SwiftData vs UserDefaults
  • Keychain and ATS
  • XCTest and async tests
  • Code signing and provisioning profiles
  • App Store/TestFlight release basics
  • Privacy manifests and permissions

Scenario drills:

  • Build offline-first reading list
  • Fix slow table scrolling with remote images
  • Debug retain cycle after screen dismiss
  • Design modular app architecture
  • Add token-based API client
  • Implement SwiftUI navigation router
  • Explain migration from UIKit to SwiftUI
  • Explain Swift 6 concurrency migration
  • Improve app accessibility
  • Tell a production bug story using STAR

Behavioral story ideas:

  • App Store release issue
  • Crash or memory leak fix
  • Slow screen performance improvement
  • SwiftUI migration
  • UIKit legacy maintenance
  • API contract change
  • Accessibility improvement
  • CI/code signing failure
  • Deadline trade-off with measurable result

Good final close:

“I can build new SwiftUI features, maintain UIKit code, debug memory and performance issues, explain Swift concurrency safely, and connect architecture decisions to reliability, testability, and user experience.”


Pattern cheat sheet (quick reference)

Need Reach for
Simple network request URLSession + async/await
Request with headers/body URLRequest
Decode API response Codable DTO
Store user preference UserDefaults
Store token/password Keychain
Complex local database Core Data
Modern SwiftUI persistence SwiftData
Secure network transport ATS + HTTPS
Biometric prompt LocalAuthentication
Unit test async code async XCTest method
Mock API layer Protocol + fake repository
App distribution Code signing + provisioning profile
Offline-first UI Local DB as source of truth
Remote image list Reuse reset + cancellation + cache
Large app structure SPM feature/core modules
iOS accessibility VoiceOver + Dynamic Type + traits

Pattern cheat sheet (quick reference)

Topic Remember
UI updates Main thread / @MainActor
Shared mutable state actor or isolation
Cycles weak delegates, [weak self]
Lists Reuse identifiers, diffable data
SwiftUI state @StateObject for owned VMs
Network async URLSession + Codable
Secrets Keychain, not UserDefaults
Tests Protocol injection + XCTest

References

Official Apple documentation

On-site prep


Summary

iOS interviews combine Swift fundamentals (value types, ARC, protocols) with modern concurrency (async/await, actors, @MainActor) and honest UIKit vs SwiftUI trade-offs. Answer aloud and compare your structure to each section. Pair with senior Android prep for mobile system design.

Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive …