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)
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
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.
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)") }
}
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
}
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 }
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
}
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)
}
}
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
}
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)
}
}
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()
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)
}
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
}
}
Accessing actor properties from outside requires await:
let account = BankAccount()
await account.deposit(100)
@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)")
}
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)
}
In SwiftUI, you call this from a .task modifier:
.task {
do {
users = try await fetchUsers()
} catch {
errorMessage = error.localizedDescription
}
}
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
}
}
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
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()
}
}
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
}
}
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
}
}
Key differences from Core Data:
- No
.xcdatamodeldfile needed - Uses
@Modelmacro instead of NSManagedObject - Query with
#Predicateinstead 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)
}
}
}
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
}
}
25. How do push notifications work in iOS?
- Request permission with
UNUserNotificationCenter - Register for remote notifications
- Apple sends you a device token
- Send the token to your backend
- 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()
}
}
}
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)
}
}
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")
}
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 variableto 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?
- Use
weakorunownedreferences in closures that captureself - Use Instruments (Leaks template) to detect leaks
- Check for retain cycles between classes
- Use
[weak self]in escaping closures
class ViewModel {
func loadData() {
service.fetch { [weak self] result in
guard let self else { return }
self.data = result
}
}
}
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
- Build something before the interview. Interviewers can tell if you only read tutorials.
- Know the WHY, not just the WHAT. "We use structs because value semantics prevent shared mutable state bugs" beats "structs are value types."
- 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.
- 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:
- Daily tips on Telegram: t.me/SwiftUIDaily
- Developer tools and templates: boosty.to/swiftuidev
Good luck with your interviews. You have got this.
Top comments (0)