InterviewHack.ai
Start free
Blog/iOS Swift Interview Questions — 40 with Code and Answers

iOS Swift Interview Questions — 40 with Code and Answers

September 16, 2026

iosswift

40 iOS/Swift interview questions asked at real companies: ARC, protocols, Swift concurrency (async/await, actors), UIKit vs SwiftUI, Core Data, testing. With Swift code.

iOS Swift Interview Questions — 40 with Code and Answers

If you are preparing for an iOS developer interview at a startup, a consultancy, or a FAANG-adjacent company, this guide gives you exactly what you need: real questions, real Swift code, and the reasoning that separates a confident candidate from one who memorized a definition.

Each section below covers a topic cluster. Read the question, study the answer, and pay attention to the "What interviewers actually want" notes — those are the parts that make the difference.


Swift Optionals

1. What is an Optional in Swift and why does the language have them?

An Optional is a type that represents either the presence of a value or the complete absence of one (nil). Swift makes optionality explicit at the type level; a variable of type String can never be nil, but String? can.

swift
var name: String? = "Ana"
name = nil // perfectly valid

var required: String = "Pedro"
// required = nil // compile error

What interviewers actually want: They want to hear that Optionals eliminate an entire class of null-pointer crashes by making the possibility of absence a compile-time concern rather than a runtime surprise.

2. What is the difference between `if let`, `guard let`, and force-unwrapping (`!`)?

swift
// if let — scope limited to the block
if let username = userInput {
    print("Hello, \(username)")
}

// guard let — early exit; value available after the guard
func greet(_ input: String?) {
    guard let username = input else {
        print("No input")
        return
    }
    print("Hello, \(username)") // username is non-optional here
}

// Force unwrap — crashes at runtime if nil
let count = userInput!.count // dangerous

Common mistake: Using force-unwrap in production because "it will never be nil." Interviewers mark this down every time. Prefer guard let for function-level validation and if let for conditional branches.

3. What is optional chaining and when does it return nil?

Optional chaining lets you call properties, methods, and subscripts on an optional that might be nil. The entire chain evaluates to nil if any link is nil.

swift
struct Address { var city: String }
struct User { var address: Address? }

let user: User? = User(address: Address(city: "Amsterdam"))
let city = user?.address?.city // "Amsterdam"

let noUser: User? = nil
let noCity = noUser?.address?.city // nil — no crash

4. What is the nil-coalescing operator and when should you use it?

swift
let displayName = user?.name ?? "Anonymous"

Use it to provide a default value when an optional is nil. It is syntactic sugar for optional != nil ? optional! : default.


Value Types vs Reference Types

5. What is the difference between a `struct` and a `class` in Swift?

| Feature | struct | class |

|---|---|---|

| Type | Value type | Reference type |

| Inheritance | No | Yes |

| ARC | No | Yes |

| Mutability | Explicit (mutating) | Implicit |

| Thread safety | Safer by default | Requires synchronization |

swift
struct Point { var x: Int; var y: Int }
var a = Point(x: 1, y: 2)
var b = a       // copy
b.x = 99
print(a.x)      // 1 — unchanged

class Counter { var value = 0 }
let c1 = Counter()
let c2 = c1     // same reference
c2.value = 99
print(c1.value) // 99 — mutated through c2

What interviewers actually want: A clear explanation of copy semantics vs shared state. Follow up with when you would choose a class: identity matters, you need inheritance, or you interact with an Obj-C API.

6. What is Copy-on-Write (CoW) and which Swift types use it?

Swift's standard collections (Array, Dictionary, Set, String) are value types backed by heap storage. Copying is deferred until a mutation is made — this avoids unnecessary allocations.

swift
var original = [1, 2, 3]
var copy = original       // no heap copy yet
copy.append(4)            // heap copy happens here
print(original.count)     // 3

You can implement CoW in your own types using isKnownUniquelyReferenced.

7. When would you use an `enum` with associated values instead of a struct?

Enums model a fixed set of mutually exclusive states. Associated values let each case carry different data.

swift
enum NetworkResult<T> {
    case success(T)
    case failure(Error)
    case loading
}

func handle(_ result: NetworkResult<[User]>) {
    switch result {
    case .success(let users): print(users.count)
    case .failure(let error): print(error)
    case .loading: showSpinner()
    }
}

Use an enum when the cases are exhaustive and mutually exclusive. Use a struct when you model data with multiple fields that coexist.


Generics

8. What problem do generics solve and how do you write a generic function?

Generics let you write flexible, reusable code that works with any type while preserving type safety.

swift
// Without generics — duplicated for every type
func swapInts(_ a: inout Int, _ b: inout Int) { let t = a; a = b; b = t }

// With generics
func swapValues<T>(_ a: inout T, _ b: inout T) {
    let temp = a; a = b; b = temp
}

var x = 5, y = 10
swapValues(&x, &y) // x=10, y=5

var s1 = "hello", s2 = "world"
swapValues(&s1, &s2)

9. What are type constraints in generics?

swift
func largest<T: Comparable>(_ array: [T]) -> T? {
    guard !array.isEmpty else { return nil }
    return array.max()
}

largest([3, 1, 4, 1, 5, 9]) // 9
largest(["banana", "apple"]) // "banana"

You can also use where clauses for more complex constraints:

swift
func equal<T, U>(_ lhs: T, _ rhs: U) -> Bool
    where T: Equatable, T == U {
    return lhs == rhs
}

10. What is an opaque type (`some`) and how does it differ from a protocol type (`any`)?

swift
// Opaque type — caller doesn't know the concrete type, compiler does
func makeShape() -> some Shape {
    return Circle(radius: 5)
}

// Existential — type is erased at runtime, overhead
func makeAnyShape() -> any Shape {
    return Circle(radius: 5)
}

some (opaque) preserves type identity for the compiler and is more performant. any (existential) trades type information for flexibility. Since Swift 5.7 the any keyword is explicit to make the cost visible.


Property Wrappers

11. What is a property wrapper and how do you write one?

A property wrapper adds custom storage and behavior to a property without repeating boilerplate.

swift
@propertyWrapper
struct Clamped {
    private var value: Int
    let range: ClosedRange<Int>

    var wrappedValue: Int {
        get { value }
        set { value = min(max(newValue, range.lowerBound), range.upperBound) }
    }

    init(wrappedValue: Int, _ range: ClosedRange<Int>) {
        self.range = range
        self.value = min(max(wrappedValue, range.lowerBound), range.upperBound)
    }
}

struct Settings {
    @Clamped(0...100) var volume: Int = 50
}

var s = Settings()
s.volume = 150
print(s.volume) // 100

Common examples in production: @AppStorage, @Published, @State, @Binding, @EnvironmentObject.

12. What is the difference between `@State`, `@Binding`, and `@StateObject` in SwiftUI?

  • @State — source of truth owned by a view; local, value type.
  • @Binding — a reference into another view's @State; no ownership.
  • @StateObject — source of truth for a reference type (ObservableObject); survives re-renders.
swift
struct Counter: View {
    @State private var count = 0

    var body: some View {
        VStack {
            Text("\(count)")
            IncrementButton(count: $count) // pass Binding
        }
    }
}

struct IncrementButton: View {
    @Binding var count: Int
    var body: some View {
        Button("+") { count += 1 }
    }
}

ARC and Memory Management

13. How does Automatic Reference Counting (ARC) work?

ARC tracks the number of strong references to each class instance. When the count reaches zero, ARC deallocates the instance. ARC inserts retain/release calls at compile time — there is no garbage collector running at runtime.

swift
class Dog {
    let name: String
    init(_ name: String) { self.name = name; print("init \(name)") }
    deinit { print("deinit \(name)") }
}

var d1: Dog? = Dog("Rex")  // count = 1
var d2 = d1                // count = 2
d1 = nil                   // count = 1
d2 = nil                   // count = 0 → deinit called

14. What is a retain cycle and how do you break it?

A retain cycle happens when two objects hold strong references to each other, preventing ARC from ever reaching zero.

swift
class Person {
    var name: String
    var apartment: Apartment?
    init(_ name: String) { self.name = name }
    deinit { print("\(name) deallocated") }
}

class Apartment {
    var tenant: Person?   // strong — creates a cycle
    deinit { print("apartment deallocated") }
}

var john: Person? = Person("John")
var apt: Apartment? = Apartment()
john?.apartment = apt
apt?.tenant = john
john = nil  // deinit NOT called — cycle!
apt = nil   // deinit NOT called — cycle!

Fix with weak (optional, can become nil) or unowned (non-optional, must outlive):

swift
class Apartment {
    weak var tenant: Person?  // breaks the cycle
}

15. When do you use `weak` vs `unowned`?

  • weak — the referenced object can outlive the referencer OR become nil independently. The reference is always Optional.
  • unowned — the referenced object is guaranteed to outlive the referencer. Accessing it after deallocation crashes.
swift
class Customer {
    var card: CreditCard?
}

class CreditCard {
    unowned let customer: Customer  // card can't exist without a customer
    init(customer: Customer) { self.customer = customer }
}

What interviewers actually want: Understanding of when a crash is possible with unowned. If in doubt, use weak.

16. What is a closure capture list and why is `[weak self]` important?

swift
class ViewModel {
    var onUpdate: (() -> Void)?
    var data = "hello"

    func setup() {
        // Without [weak self]: ViewModel is retained by the closure
        onUpdate = { [weak self] in
            guard let self else { return }
            print(self.data)
        }
    }
}

Without [weak self], the closure strongly captures self, and if self also holds the closure, you have a retain cycle.


Protocols and Protocol-Oriented Programming

17. What is a protocol in Swift and how does it differ from an abstract class?

A protocol defines a blueprint of methods, properties, and requirements. Any type (struct, class, enum) can conform. Unlike abstract classes, protocols support multiple conformance.

swift
protocol Drawable {
    func draw()
    var color: String { get }
}

struct Circle: Drawable {
    var color: String
    func draw() { print("Drawing circle in \(color)") }
}

18. What are protocol extensions and what problem do they solve?

Protocol extensions let you provide default implementations, eliminating repetition across conforming types.

swift
protocol Greetable {
    var name: String { get }
    func greet() -> String
}

extension Greetable {
    func greet() -> String { "Hello, I'm \(name)" } // default
}

struct User: Greetable { var name: String }
let u = User(name: "Ana")
print(u.greet()) // "Hello, I'm Ana" — no implementation needed in User

19. What is Protocol-Oriented Programming (POP)?

POP favors composition over inheritance. Instead of deep class hierarchies, you compose behavior through protocol conformances and default implementations. Apple introduced the concept at WWDC 2015.

swift
protocol Flyable { func fly() }
protocol Swimmable { func swim() }

extension Flyable { func fly() { print("flying") } }
extension Swimmable { func swim() { print("swimming") } }

struct Duck: Flyable, Swimmable {}
let d = Duck()
d.fly()   // "flying"
d.swim()  // "swimming"

20. What is `Equatable` and how do you make a custom type conform?

swift
struct Point: Equatable {
    var x: Double
    var y: Double
    // Synthesized automatically for structs with Equatable members
}

let p1 = Point(x: 1, y: 2)
let p2 = Point(x: 1, y: 2)
print(p1 == p2) // true

For classes or custom logic, implement == manually:

swift
class Box: Equatable {
    var value: Int
    init(_ v: Int) { value = v }
    static func == (lhs: Box, rhs: Box) -> Bool { lhs.value == rhs.value }
}

21. What is `Codable` and how do you customize key mapping?

Codable is a type alias for Encodable & Decodable. The compiler synthesizes conformance for types whose stored properties are all Codable.

swift
struct User: Codable {
    var firstName: String
    var age: Int

    enum CodingKeys: String, CodingKey {
        case firstName = "first_name"  // maps snake_case JSON key
        case age
    }
}

let json = #"{"first_name":"Ana","age":28}"#.data(using: .utf8)!
let user = try JSONDecoder().decode(User.self, from: json)
print(user.firstName) // "Ana"

Swift Concurrency (async/await and Actors)

22. What problem does async/await solve compared to completion handlers?

Completion handlers lead to callback pyramids, error handling scattered across branches, and bugs from forgetting to call the handler. async/await makes asynchronous code look sequential.

swift
// Old style
func fetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void) {
    URLSession.shared.dataTask(with: url) { data, _, error in
        if let error { completion(.failure(error)); return }
        // decode...
        completion(.success(user))
    }.resume()
}

// Modern async/await
func fetchUser(id: Int) async throws -> User {
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode(User.self, from: data)
}

// Call site
Task {
    do {
        let user = try await fetchUser(id: 42)
        print(user.name)
    } catch {
        print(error)
    }
}

23. What is a `Task` and what is the difference between `Task` and `Task.detached`?

  • Task { } — inherits the actor context and priority of the current scope.
  • Task.detached { } — does not inherit actor context or priority. Runs independently.
swift
@MainActor
class ViewModel {
    func load() {
        Task {
            // Still on @MainActor — safe to update UI
            let data = await fetchData()
            self.items = data
        }

        Task.detached {
            // NOT on @MainActor — don't touch UI here
            let processed = await heavyProcessing()
            await MainActor.run { self.result = processed }
        }
    }
}

24. What is an Actor and how does it prevent data races?

An actor is a reference type that serializes access to its mutable state. Only one piece of code can access an actor's internals at a time.

swift
actor BankAccount {
    private var balance: Double = 0

    func deposit(_ amount: Double) {
        balance += amount
    }

    func getBalance() -> Double {
        return balance
    }
}

let account = BankAccount()
Task { await account.deposit(100) }
Task { await account.deposit(50) }
// No data race — access is serialized by the actor

25. What is `@MainActor` and when do you use it?

@MainActor constrains code to always run on the main thread. Use it on types or methods that update the UI.

swift
@MainActor
class ProfileViewModel: ObservableObject {
    @Published var name = ""

    func load() async {
        let fetched = await networkService.fetchName()
        name = fetched // safe — always on main thread
    }
}

26. What is structured concurrency and how does `async let` work?

Structured concurrency ties task lifetimes to the scope that created them. async let starts a child task immediately and suspends to await it later.

swift
func loadDashboard() async throws -> Dashboard {
    async let user = fetchUser()
    async let posts = fetchPosts()
    async let notifications = fetchNotifications()
    
    // All three run concurrently; we await all results here
    return try await Dashboard(
        user: user,
        posts: posts,
        notifications: notifications
    )
}

27. What is `AsyncSequence` and when would you use it?

AsyncSequence is the async equivalent of Sequence. Use it for streams of values over time: WebSocket messages, Combine-like pipelines, file lines.

swift
for await message in webSocket.messages {
    handleMessage(message)
}

UIKit Lifecycle vs SwiftUI

28. What is the UIViewController lifecycle?

The key lifecycle methods in order:

  1. 1init — object created
  2. 2loadView — loads or creates the view
  3. 3viewDidLoad — view loaded, called once; good place for setup
  4. 4viewWillAppear — before appearing, called every time
  5. 5viewDidAppear — after appearing; start animations, timers
  6. 6viewWillDisappear — before disappearing; pause timers
  7. 7viewDidDisappear — after disappearing; release resources
  8. 8deinit — object deallocated
swift
override func viewDidLoad() {
    super.viewDidLoad() // always call super first
    setupUI()
    viewModel.load()
}

Common mistake: Doing expensive work in viewWillAppear that should only run once — put it in viewDidLoad.

29. How does SwiftUI's view lifecycle compare to UIKit?

SwiftUI views are value types — they are structs that describe UI, not objects that own it. The framework diffs the description and updates the real view hierarchy.

Key points:

  • onAppear ≈ viewDidAppear
  • onDisappear ≈ viewDidDisappear
  • task — like onAppear but starts an async task that is cancelled on disappear
swift
struct UserList: View {
    @StateObject var vm = UserListViewModel()

    var body: some View {
        List(vm.users) { user in UserRow(user: user) }
            .task { await vm.load() }
            .onDisappear { vm.cancel() }
    }
}

30. How do you present a UIKit view controller from SwiftUI?

Use UIViewControllerRepresentable:

swift
struct ImagePickerView: UIViewControllerRepresentable {
    @Binding var selectedImage: UIImage?

    func makeUIViewController(context: Context) -> UIImagePickerController {
        let picker = UIImagePickerController()
        picker.delegate = context.coordinator
        return picker
    }

    func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}

    func makeCoordinator() -> Coordinator { Coordinator(self) }

    class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
        let parent: ImagePickerView
        init(_ parent: ImagePickerView) { self.parent = parent }

        func imagePickerController(_ picker: UIImagePickerController,
                                   didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
            parent.selectedImage = info[.originalImage] as? UIImage
            picker.dismiss(animated: true)
        }
    }
}

Core Data and SwiftData

31. What is the NSManagedObjectContext and why does it matter for thread safety?

The NSManagedObjectContext (MOC) is a scratch pad for in-memory changes. It is not thread-safe — you must only use a context on the queue it was created on.

swift
// Background work — use performBackgroundTask
container.performBackgroundTask { context in
    let entity = MyEntity(context: context)
    entity.name = "Test"
    try? context.save()
}

// Never pass NSManagedObjects across contexts directly
// Use objectID to fetch on another context:
let objectID = entity.objectID
mainContext.perform {
    let safeObject = mainContext.object(with: objectID)
}

32. What is SwiftData and how does it differ from Core Data?

SwiftData (iOS 17+) is Apple's modern persistence framework built on top of Core Data. It uses macros for model definition and integrates naturally with Swift concurrency.

swift
import SwiftData

@Model
class Trip {
    var name: String
    var destination: String
    var startDate: Date

    init(name: String, destination: String, startDate: Date) {
        self.name = name
        self.destination = destination
        self.startDate = startDate
    }
}

// In a SwiftUI view
@Query(sort: \.startDate) var trips: [Trip]
@Environment(\.modelContext) var context

Button("Add") {
    context.insert(Trip(name: "Vacation", destination: "Paris", startDate: .now))
}

Key differences from Core Data: no .xcdatamodeld file, no subclassing NSManagedObject, macros replace manual entity setup, @Query replaces NSFetchRequest.


Networking with URLSession

33. How do you make a type-safe network request with URLSession and async/await?

swift
struct APIClient {
    let session: URLSession

    func fetch<T: Decodable>(_ type: T.Type, from url: URL) async throws -> T {
        let (data, response) = try await session.data(from: url)
        guard let http = response as? HTTPURLResponse,
              (200...299).contains(http.statusCode) else {
            throw URLError(.badServerResponse)
        }
        return try JSONDecoder().decode(T.self, from: data)
    }
}

// Usage
let client = APIClient(session: .shared)
let users = try await client.fetch([User].self, from: usersURL)

34. What is `URLSessionConfiguration` and when would you create a custom one?

swift
// Ephemeral — no caching, no cookies on disk; good for sensitive auth flows
let config = URLSessionConfiguration.ephemeral

// Background — survives app termination; for large uploads/downloads
let bgConfig = URLSessionConfiguration.background(withIdentifier: "com.app.upload")

// Custom timeout
config.timeoutIntervalForRequest = 10
config.timeoutIntervalForResource = 60

let session = URLSession(configuration: config)

XCTest and Testing

35. How do you write a unit test for a ViewModel that makes async network calls?

The key is to inject a mock conforming to a protocol, never hit a real network in unit tests.

swift
protocol UserFetching {
    func fetchUser(id: Int) async throws -> User
}

class MockUserService: UserFetching {
    var stubbedUser: User?
    var shouldThrow = false

    func fetchUser(id: Int) async throws -> User {
        if shouldThrow { throw URLError(.notConnectedToInternet) }
        return stubbedUser ?? User(id: id, name: "Mock")
    }
}

class UserViewModel {
    let service: UserFetching
    var user: User?
    init(service: UserFetching) { self.service = service }

    func load(id: Int) async {
        user = try? await service.fetchUser(id: id)
    }
}

// XCTest
final class UserViewModelTests: XCTestCase {
    func testLoadSuccess() async {
        let mock = MockUserService()
        mock.stubbedUser = User(id: 1, name: "Ana")
        let vm = UserViewModel(service: mock)

        await vm.load(id: 1)

        XCTAssertEqual(vm.user?.name, "Ana")
    }

    func testLoadFailure() async {
        let mock = MockUserService()
        mock.shouldThrow = true
        let vm = UserViewModel(service: mock)

        await vm.load(id: 1)

        XCTAssertNil(vm.user)
    }
}

36. What is `XCTestExpectation` and when do you still need it?

With async/await tests, you rarely need XCTestExpectation. But it remains useful for callback-based APIs, notifications, and delegate patterns.

swift
func testNotificationPosted() {
    let exp = expectation(forNotification: .NSManagedObjectContextDidSave,
                          object: context,
                          handler: nil)
    context.save()
    wait(for: [exp], timeout: 2.0)
}

Design Patterns

37. Explain the MVVM pattern in an iOS context.

MVVM separates Model (data + business logic), ViewModel (prepares data for the view, handles user actions), and View (displays data, emits events).

swift
// Model
struct Article: Codable, Identifiable {
    let id: Int
    let title: String
    let body: String
}

// ViewModel
@MainActor
class ArticleListViewModel: ObservableObject {
    @Published var articles: [Article] = []
    @Published var isLoading = false
    private let service: ArticleService

    init(service: ArticleService = .live) {
        self.service = service
    }

    func load() async {
        isLoading = true
        defer { isLoading = false }
        articles = (try? await service.fetchAll()) ?? []
    }
}

// View
struct ArticleListView: View {
    @StateObject var vm = ArticleListViewModel()

    var body: some View {
        List(vm.articles) { article in
            Text(article.title)
        }
        .task { await vm.load() }
        .overlay { if vm.isLoading { ProgressView() } }
    }
}

38. What is the Coordinator pattern and why is it used with UIKit?

The Coordinator pattern extracts navigation logic out of view controllers. Each coordinator owns one flow (onboarding, checkout) and creates child coordinators for sub-flows.

swift
protocol Coordinator: AnyObject {
    var childCoordinators: [Coordinator] { get set }
    var navigationController: UINavigationController { get }
    func start()
}

class AppCoordinator: Coordinator {
    var childCoordinators: [Coordinator] = []
    var navigationController: UINavigationController

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

    func start() {
        let vc = HomeViewController()
        vc.coordinator = self
        navigationController.pushViewController(vc, animated: false)
    }

    func showDetail(for item: Item) {
        let child = DetailCoordinator(nav: navigationController, item: item)
        childCoordinators.append(child)
        child.start()
    }
}

What interviewers actually want: Understanding that coordinators solve the Massive View Controller problem specifically for navigation. With SwiftUI, NavigationPath and NavigationStack often replace coordinators.

39. What is Dependency Injection and why does it matter for testability?

DI means a type receives its dependencies from outside rather than creating them. This makes it testable (inject mocks), flexible (swap implementations), and explicit (dependencies are visible in the initializer).

swift
// Bad — tightly coupled, impossible to test without hitting real network
class ProfileViewModel {
    func load() async {
        let url = URL(string: "https://api.example.com/profile")!
        let (data, _) = try! await URLSession.shared.data(from: url)
        // ...
    }
}

// Good — dependency injected
class ProfileViewModel {
    private let client: HTTPClient

    init(client: HTTPClient = URLSessionClient()) {
        self.client = client
    }

    func load() async throws -> Profile {
        return try await client.fetch(Profile.self, from: .profile)
    }
}

40. What is the Singleton pattern and what are its downsides?

A Singleton ensures only one instance exists globally.

swift
final class Analytics {
    static let shared = Analytics()
    private init() {}

    func track(_ event: String) { /* ... */ }
}

Analytics.shared.track("app_open")

Downsides: Hard to test (global state, can't inject a mock), creates hidden dependencies, thread-safety requires explicit handling. Prefer dependency injection with a shared instance at the composition root rather than a singleton accessed anywhere.


App Store Submission

41. What is the App Store review process and how do you prepare for it?

The review process checks your app against Apple's App Review Guidelines. Key things to get right:

  • Privacy: Declare all NSUsageDescription keys in Info.plist for any sensitive API (camera, location, contacts). Missing declarations cause automatic rejection.
  • App Tracking Transparency: Call ATTrackingManager.requestTrackingAuthorization before using IDFA.
  • Exports compliance: Declare encryption usage.
  • Crashless build: Submit a build with no symbolication gaps; Apple reviewers reject apps that crash on launch.
swift
// Required for camera access
// Info.plist: NSCameraUsageDescription → "We use the camera to scan documents."

import AppTrackingTransparency

func requestTracking() {
    ATTrackingManager.requestTrackingAuthorization { status in
        switch status {
        case .authorized: Analytics.shared.enableIDFA()
        default: Analytics.shared.disableIDFA()
        }
    }
}

42. What is TestFlight and how does it fit into the release workflow?

TestFlight allows distribution to up to 10,000 external testers before App Store release. Internal testers (up to 100) get builds immediately; external testers require a beta review (usually 24–48 hours). In practice:

  1. 1Archive and upload via Xcode or xcodebuild
  2. 2Assign build to internal group for smoke testing
  3. 3Submit for external beta review
  4. 4Promote the same build to App Store submission — no separate build needed

Additional Questions

43. What is `Sendable` and why was it introduced?

Sendable marks a type as safe to pass across actor and concurrency boundaries. The compiler enforces this in strict concurrency mode.

swift
struct Message: Sendable {
    let text: String  // String is Sendable
    let timestamp: Date  // Date is Sendable
}

// Classes need @unchecked Sendable if you manually guarantee safety
final class Cache: @unchecked Sendable {
    private let lock = NSLock()
    private var storage: [String: Data] = [:]

    func set(_ data: Data, for key: String) {
        lock.withLock { storage[key] = data }
    }
}

44. What is the difference between `map`, `flatMap`, and `compactMap`?

swift
let numbers = [1, 2, 3, 4]

// map — transform each element, same count
let doubled = numbers.map { $0 * 2 } // [2, 4, 6, 8]

// compactMap — transform + remove nils
let strings = ["1", "two", "3"]
let ints = strings.compactMap { Int($0) } // [1, 3]

// flatMap — transform + flatten one level
let nested = [[1, 2], [3, 4]]
let flat = nested.flatMap { $0 } // [1, 2, 3, 4]

// flatMap on Optional — chain optional transformations
let str: String? = "42"
let value: Int? = str.flatMap { Int($0) } // Optional(42)

What Every Interview Gets Wrong

Overusing classes when structs are better. Value types are the default in Swift for a reason — prefer them unless you need identity, inheritance, or Obj-C interoperability.

Forgetting [weak self] in closures. Every time you capture self in a closure stored as a property, ask: does this create a cycle?

Not using guard let for early exits. Deeply nested if let chains are hard to read. Flatten with guard.

Ignoring @MainActor. Updating @Published properties off the main thread is a bug even if it works sometimes.

Testing against real implementations. Unit tests must be fast and deterministic. Inject mocks, never hit the network.

Force-unwrapping in production code. It is almost never justified. The one legitimate use is in tests where you want to fail loudly.


Quick Reference Checklist Before the Interview

  • Explain ARC and retain cycles with a code example
  • Draw the difference between weak and unowned
  • Write an async/await function from scratch
  • Explain what an actor guarantees
  • Distinguish @State, @Binding, @StateObject, @ObservedObject, @EnvironmentObject
  • Write a unit test with a mock injected via protocol
  • Explain MVVM and where business logic lives
  • Describe what happens when two closures capture self strongly

Conclusion

iOS interviews test whether you understand not just the syntax but the reasoning behind Swift's design decisions. ARC exists because manual memory management is error-prone. Optionals exist because null is the billion-dollar mistake. Actors exist because data races are silent and deadly. When you can explain the why behind each mechanism — not just the how — you demonstrate the kind of thinking that gets you hired.

Practice writing code without autocomplete. Read your code out loud. Interviewers are watching how you think, not just whether you get the final answer right.

FAQ

What Swift topics are most commonly asked in iOS interviews?+

The most frequently tested topics are ARC and memory management (retain cycles, weak/unowned), optionals and safe unwrapping, value types vs reference types (struct vs class), Swift concurrency (async/await, actors, @MainActor), protocol-oriented programming, and the MVVM design pattern. Questions about SwiftUI property wrappers (@State, @Binding, @StateObject) have become standard in any iOS interview since 2022.

How deep do iOS interview questions go on Swift concurrency?+

Mid-level and senior interviews expect you to explain the difference between Task and Task.detached, describe what an actor is and how it prevents data races, use async let for concurrent operations, and understand @MainActor for UI updates. At senior level, expect questions on Sendable, custom AsyncSequence, and structured concurrency task cancellation.

Is Core Data still asked in iOS interviews or is SwiftData replacing it?+

Core Data is still very common in interviews because most production codebases use it and SwiftData only targets iOS 17+. You should understand NSManagedObjectContext thread safety, how to perform background saves, and how to pass data between contexts using objectID. SwiftData (@Model, @Query, ModelContext) is asked at companies targeting iOS 17+ and is increasingly common in 2025 interviews.

What design patterns do iOS interviewers focus on?+

MVVM is the dominant pattern in iOS today and is expected knowledge at every level. Coordinator is frequently asked for UIKit navigation. Dependency Injection is tested to evaluate testability awareness. The Repository pattern for data access is common at senior level. MVC is asked as a baseline, usually to discuss its limitations. Singleton is asked specifically to see if you can articulate its downsides.

How do I prepare for iOS behavioral and system design questions alongside coding questions?+

For behavioral questions, prepare examples of debugging a hard crash, refactoring a large view controller, or shipping under deadline. For system design, practice designing a feed with pagination, an offline-first notes app, or a real-time chat client. Be ready to discuss trade-offs: Core Data vs SQLite, URLSession vs third-party networking, UIKit vs SwiftUI for your target iOS version.

What is the difference between @ObservedObject and @StateObject in SwiftUI?+

@StateObject means the view owns the object — SwiftUI creates it once and keeps it alive for the view's lifetime. @ObservedObject means the view does not own it; the object was created elsewhere and passed in. The critical mistake is using @ObservedObject when you mean @StateObject: if the parent view re-renders, an @ObservedObject created inline gets re-instantiated and you lose state. Use @StateObject for the source of truth and @ObservedObject for objects passed down from a parent.

Related articles

Android Kotlin Interview Questions — 40 with Code and Answers

40 Android/Kotlin interview questions: Kotlin coroutines, Jetpack Compose, ViewModel, LiveData vs Flow, Room, dependency injection with Hilt. With Kotlin code.

Prepare for your real interview

Paste your job link: we research who's interviewing you and rehearse you live.

Start free →

Have an interview coming up? Install the live copilot →

InterviewHack.ai

Prepare for the exact interview: who's interviewing you, a tailored CV, and a real coach.

Product

JobsFree ATS checkerInterview-English checkSalary checkLATAM salary reportFree coursesBlogTailored CVSpoken practiceIt's free

Remote jobs

ReactPythonFull-StackLATAMArgentinaMexicoSee all →

Prepare

Spoken practiceFrontendBackendAI EngineerBy companySell with your CV

Company

For employersAboutContactPrivacyTerms

© 2026 InterviewHack.ai · Your CV is yours. Never used to train anything. · A product of IA-PTY