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.
var name: String? = "Ana"
name = nil // perfectly valid
var required: String = "Pedro"
// required = nil // compile errorWhat 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 (`!`)?
// 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 // dangerousCommon 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.
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 crash4. What is the nil-coalescing operator and when should you use it?
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 |
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 c2What 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.
var original = [1, 2, 3]
var copy = original // no heap copy yet
copy.append(4) // heap copy happens here
print(original.count) // 3You 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.
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.
// 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?
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:
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`)?
// 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.
@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) // 100Common 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.
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.
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 called14. 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.
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):
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.
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?
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.
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.
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 User19. 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.
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?
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) // trueFor classes or custom logic, implement == manually:
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.
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.
// 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.
@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.
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 actor25. 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.
@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.
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.
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
init— object created - 2
loadView— loads or creates the view - 3
viewDidLoad— view loaded, called once; good place for setup - 4
viewWillAppear— before appearing, called every time - 5
viewDidAppear— after appearing; start animations, timers - 6
viewWillDisappear— before disappearing; pause timers - 7
viewDidDisappear— after disappearing; release resources - 8
deinit— object deallocated
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≈viewDidAppearonDisappear≈viewDidDisappeartask— likeonAppearbut starts an async task that is cancelled on disappear
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:
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.
// 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.
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?
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?
// 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.
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.
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).
// 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.
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).
// 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.
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
NSUsageDescriptionkeys inInfo.plistfor any sensitive API (camera, location, contacts). Missing declarations cause automatic rejection. - App Tracking Transparency: Call
ATTrackingManager.requestTrackingAuthorizationbefore using IDFA. - Exports compliance: Declare encryption usage.
- Crashless build: Submit a build with no symbolication gaps; Apple reviewers reject apps that crash on launch.
// 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:
- 1Archive and upload via Xcode or
xcodebuild - 2Assign build to internal group for smoke testing
- 3Submit for external beta review
- 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.
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`?
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
weakandunowned - 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
selfstrongly
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.