Master iOS technical interviews with comprehensive practice covering Swift, UIKit, SwiftUI, concurrency, memory management, and system frameworks. Use when preparing for iOS developer interviews, practicing technical questions, or refreshing iOS fundamentals.
Comprehensive iOS technical interview preparation covering all the topics that top companies like Apple, Meta, Google, Netflix, and Spotify ask about.
Value Types vs Reference Types
// Structs are value types - copied on assignment
struct Point {
var x: Int
var y: Int
}
var p1 = Point(x: 10, y: 20)
var p2 = p1 // Copy is made
p2.x = 100 // Only p2 is modified
// p1.x is still 10
// Classes are reference types - shared reference
class Person {
var name: String
init(name: String) { self.name = name }
}
var person1 = Person(name: "Alice")
var person2 = person1 // Same reference
person2.name = "Bob" // Both point to same object
// person1.name is now "Bob"
Interview Question: When would you choose a struct over a class?
Closures and Capture Lists
class DataManager {
var data: [String] = []
var onComplete: (() -> Void)?
func fetchData() {
// WRONG: Strong reference cycle
networkCall {
self.data = $0
self.onComplete?()
}
// CORRECT: Weak capture
networkCall { [weak self] result in
guard let self else { return }
self.data = result
self.onComplete?()
}
}
}
Interview Question: Explain [weak self] vs [unowned self]
weak: Optional reference, becomes nil when object deallocates (safe)unowned: Non-optional, crashes if object deallocates (use when you're certain about lifecycle)The Retain Cycle Problem
class Parent {
var child: Child?
deinit { print("Parent deallocated") }
}
class Child {
var parent: Parent? // Creates retain cycle!
// Should be: weak var parent: Parent?
deinit { print("Child deallocated") }
}
var parent: Parent? = Parent()
var child: Child? = Child()
parent?.child = child
child?.parent = parent
parent = nil // Neither deallocates!
child = nil // Memory leak
Common Interview Questions:
Actors and Data Races
// Actor provides automatic synchronization
actor BankAccount {
private 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
}
func getBalance() -> Double {
return balance
}
}
// Usage requires await at isolation boundary
let account = BankAccount()
await account.deposit(100)
let balance = await account.getBalance()
Interview Question: What problem do actors solve?
await keyword marks potential suspension pointsTask Groups and Structured Concurrency
func fetchAllUsers() async throws -> [User] {
let userIDs = ["1", "2", "3", "4", "5"]
return try await withThrowingTaskGroup(of: User.self) { group in
for id in userIDs {
group.addTask {
try await fetchUser(id: id)
}
}
var users: [User] = []
for try await user in group {
users.append(user)
}
return users
}
}
View Controller Lifecycle
1. init(coder:) or init(nibName:bundle:)
2. loadView() - only override if creating views programmatically
3. viewDidLoad() - one-time setup, views are loaded
4. viewWillAppear(_:) - every time view is about to appear
5. viewDidAppear(_:) - view is visible, start animations
6. viewWillDisappear(_:) - save data, stop timers
7. viewDidDisappear(_:) - cleanup
Interview Question: Where would you make a network request?
viewDidLoad() for initial dataviewWillAppear() if data needs refreshing each timeloadView() - that's for view creation only@Observable vs ObservableObject
// NEW: @Observable (iOS 17+) - Simpler, automatic tracking
@Observable
class UserViewModel {
var name: String = ""
var email: String = ""
var isLoading: Bool = false
}
struct ProfileView: View {
var viewModel: UserViewModel // No wrapper needed!
var body: some View {
if viewModel.isLoading {
ProgressView()
} else {
Text(viewModel.name)
}
}
}
// OLD: ObservableObject - Explicit publishing
class LegacyViewModel: ObservableObject {
@Published var name: String = ""
}
struct LegacyView: View {
@StateObject var viewModel = LegacyViewModel()
}
Property Wrapper Decision Tree:
@State: Simple value types owned by the view@Binding: Two-way connection to parent's state@Environment: System or custom environment values@Observable model: Pass directly, no wrapper needed@StateObject: Create and own an ObservableObject@ObservedObject: Receive an ObservableObject from parentURLSession Best Practices
func fetchUser(id: String) async throws -> User {
let url = URL(string: "https://api.example.com/users/\(id)")!
var request = URLRequest(url: url)
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
200..<300 ~= httpResponse.statusCode else {
throw NetworkError.invalidResponse
}
return try JSONDecoder().decode(User.self, from: data)
}
Main Thread Violations
// WRONG: UI update on background thread
URLSession.shared.dataTask(with: url) { data, _, _ in
self.imageView.image = UIImage(data: data!) // Crash!
}
// CORRECT: Dispatch to main
URLSession.shared.dataTask(with: url) { data, _, _ in
DispatchQueue.main.async {
self.imageView.image = UIImage(data: data!)
}
}
// BEST: Use @MainActor
@MainActor
func updateUI(with image: UIImage) {
imageView.image = image
}
Ask me to: