Toolmingo
Guides26 min read

50 iOS Interview Questions (With Answers)

Top iOS interview questions with clear answers and code examples — covering Swift, SwiftUI, UIKit, memory management, concurrency, and architecture patterns.

iOS interviews test Swift proficiency, UIKit and SwiftUI knowledge, memory management, concurrency, architecture patterns, and platform-specific APIs. This guide covers the 50 most common questions with clear answers and code examples.

Quick reference

Topic Most asked questions
Swift Optionals, closures, protocols, generics, struct vs class
Memory ARC, retain cycles, weak vs unowned
UIKit ViewController lifecycle, Auto Layout, delegation pattern
SwiftUI @State, @Binding, @ObservableObject, view lifecycle
Concurrency async/await, GCD, actors, Task
Architecture MVVM, MVC, Clean Architecture, Coordinator
Networking URLSession, Codable, error handling
Persistence Core Data, UserDefaults, Keychain, SwiftData

Swift fundamentals

1. What is an optional in Swift and how do you safely unwrap it?

An optional represents a value that may be absent — it is either some(Value) or none. Swift forces you to handle the absence explicitly before using the value.

var name: String? = "Alice"

// 1. Optional binding (preferred)
if let name = name {
    print("Hello, \(name)")
}

// 2. Guard let (early exit)
func greet(_ name: String?) {
    guard let name = name else { return }
    print("Hello, \(name)")
}

// 3. Nil coalescing
let display = name ?? "Unknown"

// 4. Optional chaining
let length = name?.count        // Int? — nil if name is nil

// 5. Forced unwrap — only when nil is impossible
let forced = name!              // crashes if nil — avoid in production

Never use ! unless you are 100% certain the value exists (e.g., IBOutlets after viewDidLoad).


2. What is the difference between struct and class in Swift?

Feature struct (value type) class (reference type)
Memory Stack (usually) Heap
Copying Copy on assignment Shared reference
Inheritance No Yes (single)
Deinitialiser No Yes (deinit)
Mutability mutating methods Methods mutate freely
ARC Not managed by ARC Managed by ARC
Thread safety Copies are independent Shared state — needs sync
struct Point { var x: Int; var y: Int }
class Counter { var count = 0 }

var p1 = Point(x: 0, y: 0)
var p2 = p1
p2.x = 10
print(p1.x)  // 0 — independent copy

var c1 = Counter()
var c2 = c1
c2.count = 10
print(c1.count)  // 10 — same object

Rule of thumb: prefer struct by default. Use class for identity semantics, inheritance, or Objective-C interop.


3. What is ARC (Automatic Reference Counting)?

ARC tracks the number of strong references to each class instance. When the count drops to zero, ARC deallocates the instance.

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

var dog1: Dog? = Dog(name: "Rex")   // count = 1
var dog2 = dog1                      // count = 2
dog1 = nil                           // count = 1
dog2 = nil                           // count = 0 → deinit called

ARC is compile-time — there is no runtime garbage collector pause.


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

A retain cycle occurs when two objects hold strong references to each other, preventing ARC from deallocating either.

// Retain cycle
class Person {
    var pet: Pet?
    deinit { print("Person deallocated") }
}

class Pet {
    var owner: Person?              // strong — creates cycle!
    deinit { print("Pet deallocated") }
}

var person: Person? = Person()
var pet: Pet? = Pet()
person?.pet = pet
pet?.owner = person
person = nil
pet = nil
// Neither deinit is called — memory leak!

Fix with weak or unowned:

class Pet {
    weak var owner: Person?         // weak — allows deallocation
}
Reference Value type Use when
strong Default You own the object
weak Optional<T> Reference may become nil (delegate, parent VC)
unowned Non-optional Reference will never be nil during object lifetime (e.g., child → parent that always outlives it)

5. What are closures in Swift? How do you avoid capture cycles?

Closures are self-contained blocks that capture values from their surrounding scope.

// Closure capturing self — creates retain cycle if self holds the closure
class ViewModel {
    var name = "Alice"
    var onComplete: (() -> Void)?

    func setup() {
        onComplete = { [weak self] in        // capture list
            guard let self = self else { return }
            print(self.name)
        }
    }
}

Capture list rules:

  • [weak self] — self becomes optional; use when the closure may outlive self
  • [unowned self] — non-optional; use when the closure is guaranteed to not outlive self (crashes if self is nil)

6. What is a protocol in Swift? How does it differ from an abstract class?

A protocol defines a blueprint of methods, properties, and requirements. Unlike abstract classes in other languages, any type (class, struct, enum) can conform.

protocol Drawable {
    var color: String { get }
    func draw()
    func resize(by factor: Double)     // required
}

extension Drawable {
    func resize(by factor: Double) {   // default implementation
        print("Default resize \(factor)x")
    }
}

struct Circle: Drawable {
    var color = "red"
    func draw() { print("Drawing circle") }
}

Protocol vs abstract class:

Protocol Abstract class
Multiple conformance Yes No (single inheritance)
Value types Yes (struct/enum) No
Stored properties No Yes
Default implementation Yes (extension) Yes

7. What are generics in Swift?

Generics let you write flexible, reusable functions and types that work with any type satisfying given constraints.

// Generic function
func swap<T>(_ a: inout T, _ b: inout T) {
    let temp = a; a = b; b = temp
}

// Generic type with constraint
struct Stack<Element> {
    private var items: [Element] = []
    mutating func push(_ item: Element) { items.append(item) }
    mutating func pop() -> Element? { items.popLast() }
    var top: Element? { items.last }
}

// Protocol constraint
func largest<T: Comparable>(_ array: [T]) -> T? {
    array.max()
}

8. What is the difference between map, filter, and flatMap?

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

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

// filter — keep elements satisfying predicate
let evens = numbers.filter { $0 % 2 == 0 }     // [2, 4]

// reduce — aggregate
let sum = numbers.reduce(0, +)                  // 15

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

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

9. What is @escaping closure?

A closure marked @escaping can outlive the function it was passed to — it is stored and called later.

var completionHandlers: [() -> Void] = []

func addHandler(_ handler: @escaping () -> Void) {
    completionHandlers.append(handler)   // stored — outlives function
}

func fetchData(completion: @escaping (Data?) -> Void) {
    URLSession.shared.dataTask(with: url) { data, _, _ in
        completion(data)                 // called after function returns
    }.resume()
}

Non-escaping (default) closures are called synchronously within the function body.


10. What are enum associated values and how are they used?

enum Result<Success, Failure: Error> {
    case success(Success)
    case failure(Failure)
}

enum NetworkError: Error {
    case notFound
    case serverError(statusCode: Int, message: String)
    case timeout
}

func handle(_ error: NetworkError) {
    switch error {
    case .notFound:
        print("404")
    case .serverError(let code, let msg):
        print("Server \(code): \(msg)")
    case .timeout:
        print("Timed out")
    }
}

UIKit

11. What is the UIViewController lifecycle?

init(coder:) / init(nibName:bundle:)
        ↓
  loadView()          — creates view hierarchy (don't call super if overriding)
        ↓
  viewDidLoad()       — view loaded; set up subviews, bind data, register cells
        ↓
  viewWillAppear(_:)  — about to become visible (may fire multiple times)
        ↓
  viewDidAppear(_:)   — visible; start animations, timers
        ↓
  viewWillDisappear(_:)
        ↓
  viewDidDisappear(_:) — stop timers, save state
        ↓
  deinit              — remove observers
Method Common use
viewDidLoad() One-time setup, add subviews
viewWillAppear(_:) Refresh data, show/hide elements
viewDidAppear(_:) Start animations, analytics events
viewWillDisappear(_:) Save unsaved data
viewDidDisappear(_:) Stop timers, pause video

12. What is Auto Layout? Explain constraint priority.

Auto Layout resolves ambiguous or conflicting constraint sets by assigning a constraint to each view's position and size.

Priorities (1–1000):

  • required = 1000 — must be satisfied (crash if not)
  • defaultHigh = 750 — used for content compression resistance
  • defaultLow = 250 — used for content hugging
// Programmatic constraints
NSLayoutConstraint.activate([
    label.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16),
    label.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
    label.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16)
])

// Content hugging — resist expanding beyond intrinsic size
label.setContentHuggingPriority(.defaultHigh, for: .horizontal)

// Compression resistance — resist shrinking below intrinsic size
label.setContentCompressionResistancePriority(.required, for: .horizontal)

13. What is the delegation pattern in UIKit?

Delegation lets one object act on behalf of another. The delegating object holds a weak reference to its delegate to avoid retain cycles.

protocol ImagePickerDelegate: AnyObject {
    func imagePicker(_ picker: ImagePickerController, didSelect image: UIImage)
}

class ImagePickerController: UIViewController {
    weak var delegate: ImagePickerDelegate?   // weak!

    func userSelectedImage(_ image: UIImage) {
        delegate?.imagePicker(self, didSelect: image)
    }
}

class ProfileViewController: UIViewController, ImagePickerDelegate {
    func imagePicker(_ picker: ImagePickerController, didSelect image: UIImage) {
        profileImageView.image = image
    }
}

14. What is the difference between frame and bounds?

Property Coordinate space What it describes
frame Superview's coordinate system Position and size relative to parent
bounds View's own coordinate system Internal origin (usually 0,0) and size
center Superview's coordinate system Center point
let view = UIView(frame: CGRect(x: 50, y: 100, width: 200, height: 100))
print(view.frame.origin)   // (50, 100)
print(view.bounds.origin)  // (0, 0) — own coordinate space

Scroll views manipulate bounds.origin to achieve scrolling.


15. How does UITableView work? What is cell reuse?

UITableView uses a reuse queue to avoid creating a new cell for every row. Cells that scroll off-screen are placed in the queue and recycled.

class MyCell: UITableViewCell {
    static let reuseID = "MyCell"
    // subviews...
}

// Register
tableView.register(MyCell.self, forCellReuseIdentifier: MyCell.reuseID)

// Dequeue
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: MyCell.reuseID, for: indexPath) as! MyCell
    let item = items[indexPath.row]
    cell.configure(with: item)    // always reset all properties — cell may be reused
    return cell
}

Gotcha: Never set properties on a cell without resetting them — the cell may carry state from a previous row.


16. What is prepareForReuse() used for?

Called by the table/collection view before dequeueing a cell for reuse. Reset any visual state that is set in cellForRowAt:

override func prepareForReuse() {
    super.prepareForReuse()
    imageView?.image = nil        // cancel image load
    titleLabel.text = nil
    badgeView.isHidden = true
    // cancel any async tasks started in configure()
}

SwiftUI

17. What are the main property wrappers in SwiftUI?

Wrapper Scope Purpose
@State Local to view Simple value owned by view
@Binding Passed from parent Two-way connection to parent state
@ObservableObject / @StateObject External class Reference type with @Published properties
@EnvironmentObject Environment Shared across view hierarchy
@Environment Built-in values colorScheme, dismiss, locale
@AppStorage UserDefaults Persist simple values
@FetchRequest Core Data Fetch managed objects
struct CounterView: View {
    @State private var count = 0          // local state

    var body: some View {
        VStack {
            Text("Count: \(count)")
            Button("+") { count += 1 }
        }
    }
}

18. What is the difference between @StateObject and @ObservedObject?

Wrapper Owns the object Use when
@StateObject Yes — view creates and owns it Creating the object in the view
@ObservedObject No — receives from outside Object passed in as parameter
class ViewModel: ObservableObject {
    @Published var title = "Hello"
}

// Parent creates and owns — use @StateObject
struct ParentView: View {
    @StateObject private var vm = ViewModel()
    var body: some View {
        ChildView(vm: vm)
    }
}

// Child receives — use @ObservedObject
struct ChildView: View {
    @ObservedObject var vm: ViewModel
    var body: some View { Text(vm.title) }
}

Never use @ObservedObject to create an object — it will be recreated on every re-render.


19. What causes a SwiftUI view to re-render?

A view re-renders when:

  1. @State property changes
  2. @ObservableObject's @Published property emits
  3. @EnvironmentObject changes
  4. Parent view re-renders and passes new values

SwiftUI diffs the view tree and only re-renders views whose inputs changed. Use equatable() or split views to minimise re-renders.

// Avoid expensive recomputation on every render
struct ExpensiveView: View {
    let items: [Item]

    var body: some View {
        List(items) { item in
            ItemRow(item: item)  // only re-renders if item changes
        }
    }
}

20. What is @Binding and when do you use it?

@Binding creates a two-way connection between a child view and its parent's state without transferring ownership.

struct ToggleRow: View {
    @Binding var isOn: Bool           // receives reference to parent state

    var body: some View {
        Toggle("Enable", isOn: $isOn)
    }
}

struct SettingsView: View {
    @State private var notificationsEnabled = false

    var body: some View {
        ToggleRow(isOn: $notificationsEnabled)   // pass binding with $
    }
}

21. What are LazyVStack and LazyHStack?

Lazy stacks create child views only as they become visible on screen, making them suitable for large data sets.

ScrollView {
    LazyVStack {
        ForEach(0..<1_000) { index in
            Text("Row \(index)")   // created only when visible
        }
    }
}

Use LazyVStack/LazyHStack inside ScrollView for long lists. Prefer List for standard table-like content as it handles separators, swipe actions, and selection.


22. What is SwiftUI's Task and onAppear?

Task launches a Swift concurrency task tied to the view's lifecycle. .onAppear runs code when the view appears.

struct UserView: View {
    @State private var user: User?

    var body: some View {
        Group {
            if let user = user {
                Text(user.name)
            } else {
                ProgressView()
            }
        }
        .task {                        // preferred for async work
            user = await fetchUser()
        }
        // .onAppear { } — for synchronous setup
    }
}

.task automatically cancels when the view disappears, unlike .onAppear.


Concurrency

23. What is GCD (Grand Central Dispatch)?

GCD dispatches work on thread pools without you managing threads directly.

// Main queue — UI updates
DispatchQueue.main.async {
    self.label.text = "Done"
}

// Background queue — heavy work
DispatchQueue.global(qos: .userInitiated).async {
    let data = processLargeFile()
    DispatchQueue.main.async {
        self.display(data)            // back to main for UI
    }
}

// After delay
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
    showBanner()
}

QoS levels (high → low priority): .userInteractive > .userInitiated > .default > .utility > .background.


24. What is async/await in Swift?

Swift concurrency (Swift 5.5+) lets you write asynchronous code in a linear style without callbacks.

// Mark function as async
func fetchUser(id: Int) async throws -> User {
    let url = URL(string: "https://api.example.com/users/\(id)")!
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode(User.self, from: data)
}

// Call with await
func loadProfile() async {
    do {
        let user = try await fetchUser(id: 42)
        print(user.name)
    } catch {
        print("Error:", error)
    }
}

// Run from synchronous context
Task {
    await loadProfile()
}

25. What is an Actor in Swift?

An actor protects mutable state from concurrent access. All access to an actor's properties is serialised, preventing data races.

actor BankAccount {
    private var balance: Double = 0

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

    func withdraw(_ amount: Double) throws {
        guard balance >= amount else { throw BankError.insufficient }
        balance -= amount
    }

    var currentBalance: Double { balance }
}

let account = BankAccount()
Task { await account.deposit(100) }
Task { try? await account.withdraw(30) }

@MainActor ensures code runs on the main thread — apply to ViewModels updating UI.

@MainActor
class ProfileViewModel: ObservableObject {
    @Published var user: User?

    func load() async {
        user = await fetchUser()    // UI update guaranteed on main thread
    }
}

26. What is async let and TaskGroup?

async let runs multiple async tasks concurrently:

async let user = fetchUser(id: 1)
async let posts = fetchPosts(userId: 1)
let (u, p) = try await (user, posts)   // both run concurrently

TaskGroup handles dynamic numbers of concurrent tasks:

let results = try await withThrowingTaskGroup(of: User.self) { group in
    for id in ids {
        group.addTask { try await fetchUser(id: id) }
    }
    var users: [User] = []
    for try await user in group {
        users.append(user)
    }
    return users
}

Networking

27. How do you make a network request with URLSession?

struct User: Codable {
    let id: Int
    let name: String
    let email: String
}

func fetchUser(id: Int) async throws -> User {
    guard let url = URL(string: "https://jsonplaceholder.typicode.com/users/\(id)") else {
        throw URLError(.badURL)
    }
    let (data, response) = try await URLSession.shared.data(from: url)
    guard let httpResponse = response as? HTTPURLResponse,
          (200...299).contains(httpResponse.statusCode) else {
        throw URLError(.badServerResponse)
    }
    return try JSONDecoder().decode(User.self, from: data)
}

28. What is Codable in Swift?

Codable is a type alias for Encodable & Decodable. Swift synthesises the implementation when property names match JSON keys.

struct Article: Codable {
    let id: Int
    let title: String
    let publishedAt: Date               // needs dateDecodingStrategy

    enum CodingKeys: String, CodingKey {
        case id
        case title
        case publishedAt = "published_at"   // custom key mapping
    }
}

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
decoder.keyDecodingStrategy = .convertFromSnakeCase   // auto snake_case mapping

let article = try decoder.decode(Article.self, from: jsonData)

// Encode back to JSON
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let data = try encoder.encode(article)

29. How do you handle errors from a network request?

enum APIError: Error, LocalizedError {
    case invalidURL
    case httpError(statusCode: Int)
    case decodingError(Error)
    case networkError(Error)

    var errorDescription: String? {
        switch self {
        case .invalidURL: return "Invalid URL"
        case .httpError(let code): return "HTTP \(code)"
        case .decodingError(let e): return "Decoding: \(e.localizedDescription)"
        case .networkError(let e): return e.localizedDescription
        }
    }
}

func fetch<T: Decodable>(_ type: T.Type, from url: URL) async -> Result<T, APIError> {
    do {
        let (data, response) = try await URLSession.shared.data(from: url)
        guard let http = response as? HTTPURLResponse else {
            return .failure(.networkError(URLError(.badServerResponse)))
        }
        guard (200...299).contains(http.statusCode) else {
            return .failure(.httpError(statusCode: http.statusCode))
        }
        do {
            let decoded = try JSONDecoder().decode(T.self, from: data)
            return .success(decoded)
        } catch {
            return .failure(.decodingError(error))
        }
    } catch {
        return .failure(.networkError(error))
    }
}

Architecture

30. What is the MVC pattern in iOS?

Component Responsibility Example
Model Data and business logic User, APIService
View Display and user interaction UITableViewCell, Storyboard
Controller Mediates between Model and View UIViewController

Problem: ViewControllers tend to become "Massive ViewControllers" because UIKit blends controller and view concerns. MVVM addresses this.


31. What is MVVM in iOS?

Component Responsibility
Model Data structures and business logic
View UI — renders ViewModel output, sends user actions
ViewModel Transforms model data for display; does not import UIKit
// Model
struct User: Codable {
    let id: Int
    let name: String
}

// ViewModel
@MainActor
class UserViewModel: ObservableObject {
    @Published var displayName = ""
    @Published var isLoading = false
    @Published var errorMessage: String?

    private let service: UserService

    init(service: UserService = .shared) {
        self.service = service
    }

    func load(id: Int) async {
        isLoading = true
        defer { isLoading = false }
        do {
            let user = try await service.fetchUser(id: id)
            displayName = user.name.uppercased()
        } catch {
            errorMessage = error.localizedDescription
        }
    }
}

// View (SwiftUI)
struct UserView: View {
    @StateObject private var vm = UserViewModel()

    var body: some View {
        Group {
            if vm.isLoading { ProgressView() }
            else { Text(vm.displayName) }
        }
        .task { await vm.load(id: 1) }
    }
}

32. What is the Coordinator pattern?

Coordinators handle navigation logic, keeping ViewControllers free of routing code.

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

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

    init(nav: UINavigationController) {
        navigationController = nav
    }

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

    func showDetail(for item: Item) {
        let vc = DetailViewController(item: item)
        navigationController.pushViewController(vc, animated: true)
    }
}

33. What is dependency injection in iOS?

Dependency injection passes dependencies into an object rather than having it create them internally.

// Without DI — hard to test
class LoginViewModel {
    let service = AuthService()     // hard dependency
}

// With DI — testable
class LoginViewModel {
    let service: AuthServiceProtocol

    init(service: AuthServiceProtocol = AuthService()) {
        self.service = service
    }
}

// Test
class MockAuthService: AuthServiceProtocol {
    var loginResult: Result<User, Error> = .success(User.mock)
    func login(email: String, password: String) async throws -> User {
        return try loginResult.get()
    }
}

let vm = LoginViewModel(service: MockAuthService())

Data Persistence

34. When do you use UserDefaults vs Keychain vs Core Data?

Storage Use for Encrypted Max size
UserDefaults Preferences, feature flags, small primitives No (plist) Small
Keychain Passwords, tokens, biometric keys Yes Small
Core Data Structured relational data, large datasets Optional (NSFileProtection) Large
Files Images, documents, audio Optional Large
SwiftData Core Data replacement (iOS 17+) Optional Large
// UserDefaults
UserDefaults.standard.set("dark", forKey: "theme")
let theme = UserDefaults.standard.string(forKey: "theme") ?? "light"

// Keychain (using Security framework or KeychainAccess library)
import Security
let data = token.data(using: .utf8)!
let query: [String: Any] = [
    kSecClass as String: kSecClassGenericPassword,
    kSecAttrAccount as String: "userToken",
    kSecValueData as String: data
]
SecItemAdd(query as CFDictionary, nil)

35. What is Core Data?

Core Data is Apple's object graph and persistence framework. It provides:

  • SQLite, binary, or in-memory storage
  • Managed object context (MOC) as a scratchpad
  • NSFetchRequest for querying
  • Undo/redo support
// Fetch
let request: NSFetchRequest<Article> = Article.fetchRequest()
request.predicate = NSPredicate(format: "isRead == false")
request.sortDescriptors = [NSSortDescriptor(keyPath: \Article.date, ascending: false)]
let articles = try context.fetch(request)

// Create and save
let article = Article(context: context)
article.title = "New Article"
article.date = Date()
try context.save()

36. What is SwiftData (iOS 17+)?

SwiftData is a Swift-native replacement for Core Data, using macros instead of NSManagedObject subclasses.

import SwiftData

@Model
class Article {
    var title: String
    var date: Date
    var isRead: Bool

    init(title: String, date: Date = .now, isRead: Bool = false) {
        self.title = title
        self.date = date
        self.isRead = isRead
    }
}

// SwiftUI integration
struct ArticleListView: View {
    @Query(sort: \Article.date, order: .reverse)
    var articles: [Article]

    @Environment(\.modelContext) private var context

    var body: some View {
        List(articles) { article in
            Text(article.title)
        }
    }
}

iOS-specific APIs

37. What is the iOS App lifecycle?

UIKit (UIApplicationDelegate):

Not Running
    ↓ launch
Inactive (foreground, not receiving events)
    ↓
Active (foreground, receiving events)
    ↓ home button / lock
Background (limited time to finish work)
    ↓ system suspends
Suspended (in memory, not executing)
    ↓ memory pressure
Not Running

SwiftUI / UISceneDelegate lifecycle phases:

Phase When
.background App launched but not visible
.inactive Transitioning or interrupted
.active Foreground and receiving events
// SwiftUI
struct MyApp: App {
    @Environment(\.scenePhase) var phase

    var body: some Scene {
        WindowGroup { ContentView() }
            .onChange(of: phase) { newPhase in
                switch newPhase {
                case .active: startTimers()
                case .background: saveState()
                default: break
                }
            }
    }
}

38. How do push notifications work on iOS?

App → request permission
            ↓
User grants → APNs registers → device token
            ↓
App sends token to your server
            ↓
Server → APNs → device
// Request permission
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, _ in
    if granted {
        DispatchQueue.main.async {
            UIApplication.shared.registerForRemoteNotifications()
        }
    }
}

// Receive token
func application(_ application: UIApplication,
                 didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    let token = deviceToken.map { String(format: "%02x", $0) }.joined()
    sendToServer(token: token)
}

// Handle foreground notification
extension AppDelegate: UNUserNotificationCenterDelegate {
    func userNotificationCenter(_ center: UNUserNotificationCenter,
                                willPresent notification: UNNotification,
                                withCompletionHandler handler: @escaping (UNNotificationPresentationOptions) -> Void) {
        handler([.banner, .sound])    // show even when app is active
    }
}

39. What is background execution in iOS?

iOS limits background execution by default. Options:

Mode Plist key Use case
Background Fetch fetch Periodic data refresh
Remote Notifications remote-notification Silent push to trigger fetch
Background Processing processing Long-running tasks via BGProcessingTask
Audio, AirPlay audio Music playback
Location updates location Navigation apps
VoIP voip Calling apps
// BGTaskScheduler (iOS 13+)
BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.app.refresh", using: nil) { task in
    handleAppRefresh(task: task as! BGAppRefreshTask)
}

func scheduleRefresh() {
    let request = BGAppRefreshTaskRequest(identifier: "com.app.refresh")
    request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
    try? BGTaskScheduler.shared.submit(request)
}

40. What is Combine framework?

Combine is Apple's reactive framework for processing values over time.

import Combine

class SearchViewModel: ObservableObject {
    @Published var query = ""
    @Published var results: [String] = []
    private var cancellables = Set<AnyCancellable>()

    init() {
        $query
            .debounce(for: .milliseconds(300), scheduler: RunLoop.main)
            .removeDuplicates()
            .filter { !$0.isEmpty }
            .sink { [weak self] query in
                self?.search(query: query)
            }
            .store(in: &cancellables)
    }

    func search(query: String) {
        // perform search
    }
}

Key operators: map, filter, flatMap, combineLatest, merge, zip, debounce, throttle, catch, retry.


Testing & Performance

41. How do you write unit tests in iOS?

import XCTest
@testable import MyApp

final class LoginViewModelTests: XCTestCase {
    var sut: LoginViewModel!
    var mockService: MockAuthService!

    override func setUp() {
        super.setUp()
        mockService = MockAuthService()
        sut = LoginViewModel(service: mockService)
    }

    override func tearDown() {
        sut = nil
        mockService = nil
        super.tearDown()
    }

    func test_login_success_updatesUser() async throws {
        // Given
        mockService.loginResult = .success(.init(id: 1, name: "Alice"))

        // When
        await sut.login(email: "a@a.com", password: "pass")

        // Then
        XCTAssertEqual(sut.user?.name, "Alice")
        XCTAssertFalse(sut.isLoading)
        XCTAssertNil(sut.errorMessage)
    }

    func test_login_failure_setsErrorMessage() async {
        // Given
        mockService.loginResult = .failure(AuthError.invalidCredentials)

        // When
        await sut.login(email: "a@a.com", password: "wrong")

        // Then
        XCTAssertNil(sut.user)
        XCTAssertNotNil(sut.errorMessage)
    }
}

42. How do you profile memory leaks in iOS?

  1. Instruments → Leaks: runs the app, detects allocations that are no longer reachable
  2. Instruments → Allocations: heap growth, retain counts
  3. Memory Graph Debugger (Xcode): pause app → capture memory graph → see reference cycles
// Common leak: strong self in closure stored on self
class VC: UIViewController {
    var onComplete: (() -> Void)?

    func setup() {
        onComplete = {
            self.doSomething()    // strong capture — potential leak
        }
        // Fix:
        onComplete = { [weak self] in
            self?.doSomething()
        }
    }
}

43. What are Swift concurrency structured tasks?

Structured concurrency means child tasks are scoped to their parent — they are cancelled when the parent is cancelled.

func loadDashboard() async throws -> Dashboard {
    async let profile = fetchProfile()
    async let feed = fetchFeed()
    async let notifications = fetchNotifications()

    // All three run concurrently; any failure cancels others
    return Dashboard(
        profile: try await profile,
        feed: try await feed,
        notifications: try await notifications
    )
}

// Task cancellation
let task = Task {
    await heavyWork()
}
task.cancel()   // sets task's isCancelled; cooperative cancellation

Advanced topics

44. What is the Responder Chain?

UIKit routes events (touches, motion, remote control) through a chain of responders: view → superview → ViewController → UIWindow → UIApplication → AppDelegate. The first responder that handles the event stops propagation.

// Custom responder action
class MyButton: UIButton {
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        // handle or call super to pass up the chain
        super.touchesBegan(touches, with: event)
    }
}

// Send action up the responder chain
UIApplication.shared.sendAction(#selector(MyAction.doSomething), to: nil, from: self, for: nil)

45. What is NSOperation vs GCD?

GCD NSOperation
Level Low-level C API Higher-level Obj-C/Swift
Dependencies Manual addDependency(_:)
Cancellation Not directly cancel()
KVO observation No Yes
Reuse No Yes (subclass)
Concurrency limit DispatchSemaphore maxConcurrentOperationCount

Use GCD for simple async tasks. Use NSOperation when you need dependencies, cancellation, or priority management.


46. What is @objc and dynamic in Swift?

  • @objc — exposes Swift declarations to Objective-C runtime (required for selectors, delegate methods in ObjC protocols)
  • dynamic — forces dispatch through the ObjC runtime (enables method swizzling, KVO)
class MyView: UIView {
    @objc dynamic var progress: Float = 0    // KVO observable

    @objc func handleTap(_ gesture: UITapGestureRecognizer) {
        // selector-based target/action requires @objc
    }
}

// KVO observation
myView.observe(\.progress, options: [.new]) { view, change in
    print("Progress: \(change.newValue ?? 0)")
}

47. What is the difference between synchronous and asynchronous URLSession tasks?

URLSession's data tasks are inherently asynchronous. With async/await:

// Async — preferred (iOS 15+)
let (data, response) = try await URLSession.shared.data(from: url)

// Completion handler — older pattern (iOS < 15 or Objective-C)
URLSession.shared.dataTask(with: url) { data, response, error in
    // called on background queue — dispatch to main for UI
    DispatchQueue.main.async {
        self.updateUI(data: data)
    }
}.resume()

48. What is Sendable in Swift concurrency?

Sendable marks types that are safe to pass across actor and task boundaries without risk of data races.

// Implicitly Sendable: value types, immutable classes
struct Config: Sendable {          // safe — all properties are Sendable
    let host: String
    let port: Int
}

// Non-Sendable: class with mutable state
class Cache {
    var data: [String: Data] = [:]  // not Sendable by default
}

// Mark as @unchecked Sendable only when you manually ensure thread safety
class SafeCache: @unchecked Sendable {
    private var data: [String: Data] = [:]
    private let lock = NSLock()

    func get(_ key: String) -> Data? {
        lock.lock(); defer { lock.unlock() }
        return data[key]
    }
}

49. What is the Hashable and Identifiable protocol?

// Hashable — for use in Set and as Dictionary keys
struct Point: Hashable {
    var x: Int
    var y: Int
    // Swift synthesises hash(into:) and == when all properties are Hashable
}

var visited: Set<Point> = []
visited.insert(Point(x: 0, y: 0))

// Identifiable — required for ForEach and List in SwiftUI
struct Article: Identifiable {
    let id: UUID           // must be unique and stable
    var title: String
}

ForEach(articles) { article in    // uses article.id for diffing
    Text(article.title)
}

50. What are common iOS interview anti-patterns?

Anti-pattern Problem Fix
Massive ViewController UI, business logic, networking mixed MVVM, Coordinator
Strong delegate Retain cycle weak var delegate: Protocol?
DispatchQueue.main.sync on main Deadlock Use async
Force unwrap ! everywhere Crashes in production Optional binding, guard
UserDefaults for sensitive data Plain-text plist Keychain
UIImage(named:) in tight loop Memory spike Cache images
Creating URLSession per request Performance Shared instance or pool
Blocking main thread with Thread.sleep ANR-like freeze Async/await

iOS vs cross-platform comparison

Native iOS (Swift) React Native Flutter
Language Swift / Obj-C JavaScript / TypeScript Dart
UI Native UIKit/SwiftUI Native components via bridge Custom Skia rendering
Performance Best Good (JSI bridge) Very good
Ecosystem Apple-first npm / large community pub.dev
Code reuse iOS only iOS + Android + Web iOS + Android + Web + Desktop
Apple features All Partial Partial
App Store approval Easiest Slightly harder Slightly harder

Common mistakes

Mistake Impact Fix
Updating UI from background thread Random crashes Always dispatch to DispatchQueue.main or use @MainActor
Missing [weak self] in closures Memory leak Capture list in stored closures
Using @ObservedObject to create objects Reinitialised on re-render Use @StateObject
Not cancelling Combine subscriptions Memory leak .store(in: &cancellables)
Storing data in UserDefaults then clearing keychain Data inconsistency Single source of truth
Ignoring Task cancellation Wasted work, race conditions Check Task.isCancelled
Tight coupling to UIApplication.shared Untestable Inject via protocol
Large images not scaled down before display Memory pressure UIGraphicsImageRenderer to resize

Frequently asked questions

Q: Should I use UIKit or SwiftUI for a new project? SwiftUI for all new projects targeting iOS 16+. UIKit for projects supporting older iOS versions or requiring features not yet in SwiftUI.

Q: Is async/await compatible with Combine? Yes — use AsyncPublisher or .values property on a Combine publisher to bridge to async/await:

for await value in publisher.values {
    print(value)
}

Q: How do you handle the "purple warning" (main thread checker) in Xcode? Ensure all UIKit access happens on the main thread. Use @MainActor, DispatchQueue.main.async, or the Main Thread Checker in the Scheme diagnostics.

Q: What is the difference between Task.detached and Task {}? Task {} inherits the actor context and priority of its parent. Task.detached creates an unstructured task with no inherited context — use sparingly.

Q: How does iOS handle memory warnings? The system calls applicationDidReceiveMemoryWarning and didReceiveMemoryWarning on all active ViewControllers. Respond by freeing caches and non-visible resources.

Q: What is SF Symbols? A library of 5,000+ system icons provided by Apple, designed to work with the San Francisco font at any weight and size:

Image(systemName: "heart.fill")
    .symbolRenderingMode(.multicolor)
    .font(.title)

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools