DEV Community

Top 30 iOS Interview Questions and Answers (2026 Edition)

iOS interviews in 2026 are different from what they were even two years ago. Companies now expect you to know SwiftUI as the primary framework, not just UIKit. They ask about Swift concurrency (async/await) as a baseline, not a bonus. And if you cannot explain @observable vs ObservableObject, that is a red flag.

I have been through these interviews myself and helped others prepare. Here are the 30 questions that keep coming up, with answers that actually make sense.

Swift Fundamentals

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

Short answer: Structs are value types. Classes are reference types.

Better answer: When you assign a struct to a new variable, Swift copies the entire value. When you assign a class instance, both variables point to the same object in memory.

struct Point {
    var x: Int
    var y: Int
}

var a = Point(x: 1, y: 2)
var b = a
b.x = 10
// a.x is still 1 (independent copy)

class Person {
    var name: String
    init(name: String) { self.name = name }
}

let p1 = Person(name: "Alice")
let p2 = p1
p2.name = "Bob"
// p1.name is now "Bob" (same reference)
Enter fullscreen mode Exit fullscreen mode

When to use which: Default to structs. Use classes when you need inheritance, reference semantics, or interop with Objective-C.

2. What are optionals and how do you unwrap them?

An optional is a type that can hold either a value or nil. Swift uses optionals to make null safety a compile-time guarantee instead of a runtime crash.

Five ways to unwrap:

let name: String? = "Alice"

// 1. Optional binding (if let)
if let name { print(name) }

// 2. Guard let (early exit)
guard let name else { return }

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

// 4. Optional chaining
let count = name?.count

// 5. Force unwrap (avoid this)
let forced = name! // Crashes if nil
Enter fullscreen mode Exit fullscreen mode

The interviewer wants to hear that you almost never use force unwrapping, and that guard let is preferred in functions for early returns.

3. Explain the difference between let and var.

let declares a constant. var declares a variable. But here is the nuance: for reference types (classes), let only means the reference cannot change. The object itself can still be mutated.

let array = NSMutableArray()
array.add("Hello") // This works! The reference is constant, not the contents.
Enter fullscreen mode Exit fullscreen mode

4. What is a protocol in Swift?

A protocol defines a contract. Any type that conforms to it must implement the required properties and methods.

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

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

Protocols are the backbone of Swift. SwiftUI's View is a protocol. Codable is a protocol. Identifiable is a protocol. Understanding protocol-oriented programming is non-negotiable for iOS interviews.

5. What are generics and why do they matter?

Generics let you write functions and types that work with any type, while keeping type safety.

func swapValues<T>(_ a: inout T, _ b: inout T) {
    let temp = a
    a = b
    b = temp
}
Enter fullscreen mode Exit fullscreen mode

Real-world example: Array<Element> is generic. Optional<Wrapped> is generic. Without generics, you would need separate implementations for every type.

6. Explain closures in Swift.

Closures are self-contained blocks of functionality. They capture values from their surrounding context.

let numbers = [3, 1, 4, 1, 5]
let sorted = numbers.sorted { $0 < $1 }
Enter fullscreen mode Exit fullscreen mode

Key concepts: closures capture variables by reference (not value), @escaping means the closure outlives the function call, and [weak self] prevents retain cycles.

7. What is ARC and how does it work?

ARC (Automatic Reference Counting) manages memory for class instances. Every time you create a strong reference to an object, its reference count increases. When it drops to zero, the object is deallocated.

The problem: retain cycles. Two objects holding strong references to each other will never be deallocated.

class Parent {
    var child: Child?
}

class Child {
    weak var parent: Parent? // weak breaks the cycle
}
Enter fullscreen mode Exit fullscreen mode

Use weak for delegates and parent references. Use unowned when you are certain the reference will never be nil during its lifetime.

SwiftUI Questions

8. What is the difference between @State, @Binding, and @Environment?

This is probably the most common SwiftUI question.

  • @State: owns the data. Used in the view that creates and manages the value.
  • @Binding: borrows the data. A two-way connection to someone else's @State.
  • @Environment: reads shared data from the environment (like color scheme, locale, or custom values).
struct ParentView: View {
    @State private var isOn = false // Owns it

    var body: some View {
        ToggleView(isOn: $isOn) // Passes binding
    }
}

struct ToggleView: View {
    @Binding var isOn: Bool // Borrows it

    var body: some View {
        Toggle("Switch", isOn: $isOn)
    }
}
Enter fullscreen mode Exit fullscreen mode

9. Explain @observable vs ObservableObject.

@Observable (iOS 17+) is the modern replacement for ObservableObject + @Published. It is simpler and more performant.

// Old way (pre-iOS 17)
class OldViewModel: ObservableObject {
    @Published var count = 0
}

// New way (iOS 17+)
@Observable
class NewViewModel {
    var count = 0 // No @Published needed
}
Enter fullscreen mode Exit fullscreen mode

With @Observable, SwiftUI tracks which properties each view actually reads and only re-renders when those specific properties change. With ObservableObject, any @Published change re-renders all observing views.

10. What is NavigationStack and why did it replace NavigationView?

NavigationStack (iOS 16+) replaced NavigationView because it supports programmatic navigation through a path-based system.

@State private var path = NavigationPath()

NavigationStack(path: $path) {
    List(items) { item in
        NavigationLink(value: item) {
            Text(item.name)
        }
    }
    .navigationDestination(for: Item.self) { item in
        DetailView(item: item)
    }
}
Enter fullscreen mode Exit fullscreen mode

You can push views programmatically with path.append(item) and pop with path.removeLast(). This was nearly impossible with the old NavigationView.

11. How does SwiftUI's diffing algorithm work?

SwiftUI compares the previous view tree with the new one after a state change. It only updates the parts that actually changed. This is why views must be structs (value types) and why Identifiable is important for lists.

When you use ForEach with Identifiable items, SwiftUI can track which items were added, removed, or reordered, instead of rebuilding the entire list.

12. What is a ViewModifier and when would you create a custom one?

A ViewModifier encapsulates reusable view modifications.

struct CardStyle: ViewModifier {
    func body(content: Content) -> some View {
        content
            .padding()
            .background(.regularMaterial)
            .clipShape(RoundedRectangle(cornerRadius: 12))
            .shadow(radius: 4)
    }
}

extension View {
    func cardStyle() -> some View {
        modifier(CardStyle())
    }
}

// Usage
Text("Hello").cardStyle()
Enter fullscreen mode Exit fullscreen mode

Create custom modifiers when you find yourself applying the same chain of modifiers in multiple places.

Concurrency & Networking

13. Explain async/await in Swift.

Async/await is Swift's structured concurrency system. It replaces completion handlers with linear, readable code.

// Old way
func fetchUser(completion: @escaping (User?) -> Void) {
    URLSession.shared.dataTask(with: url) { data, _, _ in
        // decode and call completion
    }.resume()
}

// New way
func fetchUser() async throws -> User {
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode(User.self, from: data)
}
Enter fullscreen mode Exit fullscreen mode

Key concepts: async marks a function as asynchronous. await suspends execution until the result is ready. Task creates an asynchronous context from synchronous code.

14. What is an Actor in Swift?

Actors are reference types that protect their mutable state from data races. Only one task can access an actor's state at a time.

actor BankAccount {
    var balance: Double = 0

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

    func withdraw(_ amount: Double) -> Bool {
        guard balance >= amount else { return false }
        balance -= amount
        return true
    }
}
Enter fullscreen mode Exit fullscreen mode

Accessing actor properties from outside requires await:

let account = BankAccount()
await account.deposit(100)
Enter fullscreen mode Exit fullscreen mode

@MainActor is a special global actor that ensures code runs on the main thread, which is required for all UI updates.

15. How do you handle errors in Swift?

Swift uses typed error handling with throw, try, and catch.

enum NetworkError: Error {
    case invalidURL
    case noData
    case decodingFailed
}

func fetchData() throws -> Data {
    guard let url = URL(string: "https://api.example.com")
    else { throw NetworkError.invalidURL }

    // ...
}

do {
    let data = try fetchData()
} catch NetworkError.invalidURL {
    print("Bad URL")
} catch {
    print("Unknown error: \(error)")
}
Enter fullscreen mode Exit fullscreen mode

16. Explain URLSession and how you would make a network request.

func fetchUsers() async throws -> [User] {
    let url = URL(string: "https://api.example.com/users")!
    let (data, response) = try await URLSession.shared.data(from: url)

    guard let httpResponse = response as? HTTPURLResponse,
          httpResponse.statusCode == 200
    else { throw NetworkError.badResponse }

    return try JSONDecoder().decode([User].self, from: data)
}
Enter fullscreen mode Exit fullscreen mode

In SwiftUI, you call this from a .task modifier:

.task {
    do {
        users = try await fetchUsers()
    } catch {
        errorMessage = error.localizedDescription
    }
}
Enter fullscreen mode Exit fullscreen mode

Architecture & Patterns

17. What is MVVM and why is it popular in SwiftUI?

MVVM stands for Model-View-ViewModel.

  • Model: Data structures (structs, usually Codable)
  • View: SwiftUI views that display data
  • ViewModel: The bridge. Holds state, handles logic, talks to services.

MVVM works naturally with SwiftUI because @Observable ViewModels trigger view updates automatically. The View never touches business logic directly.

18. What is dependency injection and how do you implement it in SwiftUI?

Dependency injection means passing dependencies into an object instead of creating them internally.

// Bad: hard dependency
class UserViewModel {
    let service = UserService() // Can't test this
}

// Good: injected dependency
class UserViewModel {
    let service: UserServiceProtocol

    init(service: UserServiceProtocol) {
        self.service = service
    }
}
Enter fullscreen mode Exit fullscreen mode

In SwiftUI, you can use @Environment for app-wide dependencies:

@Observable
class AppState {
    var currentUser: User?
}

// In App
ContentView()
    .environment(AppState())

// In any child view
@Environment(AppState.self) var appState
Enter fullscreen mode Exit fullscreen mode

19. Explain the Coordinator pattern.

The Coordinator pattern separates navigation logic from views. Instead of views deciding where to navigate, a Coordinator object manages the flow.

This is more common in UIKit apps, but in SwiftUI you can implement it using NavigationPath:

@Observable
class AppCoordinator {
    var path = NavigationPath()

    func showDetail(for item: Item) {
        path.append(item)
    }

    func goBack() {
        path.removeLast()
    }
}
Enter fullscreen mode Exit fullscreen mode

20. What is the Repository pattern?

The Repository pattern abstracts data access. Your ViewModel does not know (or care) whether data comes from a network API, a local database, or a cache.

protocol UserRepository {
    func getUsers() async throws -> [User]
    func getUser(id: String) async throws -> User
}

class RemoteUserRepository: UserRepository {
    func getUsers() async throws -> [User] {
        // Fetch from API
    }
}

class MockUserRepository: UserRepository {
    func getUsers() async throws -> [User] {
        return [User(name: "Test")]  // For testing
    }
}
Enter fullscreen mode Exit fullscreen mode

Data Persistence

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

SwiftData (iOS 17+) is Apple's modern persistence framework. It replaces Core Data's verbose setup with Swift macros.

@Model
class Note {
    var title: String
    var content: String
    var createdAt: Date

    init(title: String, content: String) {
        self.title = title
        self.content = content
        self.createdAt = .now
    }
}
Enter fullscreen mode Exit fullscreen mode

Key differences from Core Data:

  • No .xcdatamodeld file needed
  • Uses @Model macro instead of NSManagedObject
  • Query with #Predicate instead of NSPredicate
  • Native SwiftUI integration with @Query

22. When would you use UserDefaults vs SwiftData vs Keychain?

  • UserDefaults: Small, simple data. Settings, preferences, flags. Not for sensitive data.
  • SwiftData/Core Data: Structured data with relationships. Your app's main data store.
  • Keychain: Sensitive data. Passwords, tokens, API keys.

23. What is @Query in SwiftData?

@Query automatically fetches and observes data from your SwiftData store:

struct NotesListView: View {
    @Query(sort: \Note.createdAt, order: .reverse)
    var notes: [Note]

    var body: some View {
        List(notes) { note in
            Text(note.title)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

It updates the view automatically when data changes. No manual fetching needed.

App Lifecycle & System

24. Explain the iOS app lifecycle.

In SwiftUI, you use ScenePhase to respond to lifecycle events:

@Environment(\.scenePhase) var scenePhase

.onChange(of: scenePhase) { _, newPhase in
    switch newPhase {
    case .active:    // App is in foreground
    case .inactive:  // App is transitioning
    case .background: // App is in background
    @unknown default: break
    }
}
Enter fullscreen mode Exit fullscreen mode

25. How do push notifications work in iOS?

  1. Request permission with UNUserNotificationCenter
  2. Register for remote notifications
  3. Apple sends you a device token
  4. Send the token to your backend
  5. Backend sends notifications through APNs (Apple Push Notification service)
UNUserNotificationCenter.current().requestAuthorization(
    options: [.alert, .badge, .sound]
) { granted, error in
    if granted {
        DispatchQueue.main.async {
            UIApplication.shared.registerForRemoteNotifications()
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

26. What is a Widget and how does it work?

Widgets use WidgetKit and a Timeline Provider to display glanceable information on the home screen.

struct MyWidgetProvider: TimelineProvider {
    func getTimeline(in context: Context,
                     completion: @escaping (Timeline<Entry>) -> Void) {
        let entry = SimpleEntry(date: .now, data: fetchData())
        let timeline = Timeline(entries: [entry],
                                policy: .after(.now + 3600))
        completion(timeline)
    }
}
Enter fullscreen mode Exit fullscreen mode

Widgets cannot run arbitrary code. They display a snapshot of data that gets refreshed at intervals determined by the timeline policy.

Testing & Debugging

27. How do you write unit tests for a SwiftUI app?

Test the ViewModel, not the View:

@Test func addingTaskIncreasesCount() {
    let vm = TaskViewModel()
    vm.newTaskTitle = "Test task"
    vm.addTask()

    #expect(vm.tasks.count == 1)
    #expect(vm.tasks.first?.title == "Test task")
}
Enter fullscreen mode Exit fullscreen mode

Use Swift Testing (@Test and #expect) instead of XCTest for new projects. It has better syntax and diagnostics.

28. What tools do you use for debugging in Xcode?

  • Breakpoints: Pause execution and inspect state
  • LLDB console: po variable to print objects
  • View Hierarchy Debugger: 3D view of your UI layers
  • Instruments: Memory leaks, CPU usage, network profiling
  • SwiftUI Preview: Live preview of views while coding

29. How do you handle memory leaks in Swift?

  1. Use weak or unowned references in closures that capture self
  2. Use Instruments (Leaks template) to detect leaks
  3. Check for retain cycles between classes
  4. Use [weak self] in escaping closures
class ViewModel {
    func loadData() {
        service.fetch { [weak self] result in
            guard let self else { return }
            self.data = result
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

30. What is new in iOS 18 / Xcode 16 that you are excited about?

Good answers include:

  • Liquid Glass design system updates
  • Improved SwiftUI performance and new APIs
  • Swift Testing framework as default
  • Enhanced Xcode Previews with better performance
  • AI integration with Xcode Predictive Code Completion

This question tests whether you stay current. Follow WWDC videos and release notes.

Final Tips for Your Interview

  1. Build something before the interview. Interviewers can tell if you only read tutorials.
  2. Know the WHY, not just the WHAT. "We use structs because value semantics prevent shared mutable state bugs" beats "structs are value types."
  3. Be honest about what you do not know. Saying "I have not used that yet, but here is how I would approach it" is better than faking it.
  4. Ask good questions back. "What does your tech stack look like?" "How do you handle testing?" "What is the team structure?"

Keep Learning

I share iOS development tips, interview prep materials, and career resources regularly:

Good luck with your interviews. You have got this.

Top comments (0)