Swift Interview Questions and Answers
Optionals, protocols, ARC, structured concurrency and iOS fundamentals.
Whether you are preparing for entry-level Swift interview questions for freshers or senior software engineer interview questions addressing concurrency, scalability, and system architecture, this track provides peer-reviewed model answers with syntax walkthroughs, edge cases, and practical interview tips.
1 What are optionals and how do you safely unwrap them? Easy
An optional represents a value that may be absent: either .some(value) or .none, written nil. Non-optional types can never be nil, and the compiler forces you to unwrap.
var name: String? = "Ada"
print(name?.count ?? 0) // optional chaining + nil coalescing
if let name = name { print(name) } // optional binding
guard let name = name else { return } // early exit
let forced = name! // crashes if nil
Use optional binding, guard let, optional chaining (?.) and the nil-coalescing operator (??). Swift 5.7 supports shorthand binding with if let name. Avoid force unwrapping with ! except when nil is genuinely impossible, and the same goes for try! and as!.
Optionals are an enum under the hood, so they work with pattern matching and map/flatMap. A nested optional such as String?? distinguishes .some(nil) from nil, which matters when wrapping optional APIs.
2 What is the difference between let and var, and between structs and classes? Easy
let declares a constant binding and var a mutable one. For value types, let also prevents changing stored properties.
Structs are value types: assignment and passing make a copy, with copy-on-write for collections. Classes are reference types: assignment shares the same instance and mutations are visible to every reference.
struct Point { var x = 0 }
class Box { var value = 0 }
var p = Point()
var p2 = p
p2.x = 1 // p.x stays 0
let b = Box()
let b2 = b
b2.value = 1 // b.value is 1, same object
Classes add inheritance, deinitialisers and identity comparison with ===. Structs get memberwise initialisers and avoid shared mutable state, which is safer under concurrency. The standard library and SwiftUI favour structs. Prefer structs unless you need reference semantics or inheritance.
3 What are closures and how do capture lists work? Medium
A closure is a self-contained block of functionality that can be passed around and captures values from its surrounding context. Functions are a special case of closures.
let add = { (a: Int, b: Int) -> Int in a + b }
let names = ["Ada", "Grace"].sorted { $0 < $1 }
Syntax tools include trailing closure syntax and shorthand argument names such as $0 and $1. Capture lists control how references are captured: [weak self] or [unowned self] prevent retain cycles when a closure is stored by the object it references.
Closures are reference types, so capturing a variable captures the box that holds it. Escaping closures are annotated @escaping and outlive the function call; non-escaping is the default and allows compiler optimisations. Use closures for completion handlers, callbacks and functional pipelines like map and filter.
4 Explain ARC and how retain cycles happen. Medium
Swift uses Automatic Reference Counting: each class instance has a reference count and is deallocated when the count reaches zero. ARC is deterministic, unlike a tracing garbage collector, but it only manages references, not cycles.
A retain cycle occurs when two objects strongly reference each other, so neither count reaches zero and memory leaks.
class Node {
var next: Node?
weak var parent: Node? // breaks the cycle
}
Break cycles with weak (optional, becomes nil when the target deallocates) for a child or back reference, or unowned (non-optional, assumes the target outlives the reference and traps otherwise). In closures use a capture list: [weak self] in self?.doWork(). Delegates should be declared weak var delegate.
Value types do not participate in cycles. Find leaks with Instruments' Leaks template and the memory graph debugger.
5 How do protocols and protocol extensions enable protocol-oriented programming? Medium
A protocol declares requirements such as properties, methods, initialisers or associated types that conforming types implement. Swift encourages protocol-oriented programming: define behaviour once in extensions and compose protocols instead of building deep inheritance trees.
protocol Shape {
var area: Double { get }
}
extension Shape {
var description: String { "area: \(area)" } // default implementation
}
struct Circle: Shape {
let r: Double
var area: Double { .pi * r * r }
}
Extensions can add default methods, computed properties and conformance to other protocols. Protocols with associated types cannot be used as existential types before Swift 5.7's any; previously you used generics or type erasure.
Protocols support inheritance and composition such as some Shape & Codable. Prefer them for testability, dependency injection and sharing behaviour across unrelated types.
6 When should you use guard instead of if let? Medium
Both unwrap optionals, but guard is designed for early exit and keeps the happy path unindented. Its else branch must exit the current scope with return, break, continue or throw, and the unwrapped value stays available afterwards.
func greet(_ name: String?) {
guard let name, !name.isEmpty else { return }
print("Hello, \(name)") // name is available here
}
With if let, the binding is visible only inside the if block, and nested checks cause pyramid indentation. Use guard to validate preconditions at the top of a function and if let for optional logic in the middle.
guard also works with guard case, with guard let self inside closures, and can unwrap several optionals in one statement. Since Swift 5.7 you can write if let name or guard let name without repeating the variable name, which keeps validation concise.
7 How does error handling work in Swift? Medium
Swift uses throws functions and try for recoverable errors. Errors conform to the Error protocol, usually as enums.
enum NetworkError: Error {
case badStatus(Int)
case noData
}
func load() throws -> Data {
guard status == 200 else { throw NetworkError.badStatus(status) }
guard let data else { throw NetworkError.noData }
return data
}
do {
let data = try load()
} catch NetworkError.badStatus(let code) {
print(code)
} catch {
print(error)
}
try? converts a thrown error into an optional and discards the error; try! crashes on error and should be avoided. Functions that call throwing functions must be throws or handle errors with do/catch.
Use defer for cleanup that must run on both success and failure. Since Swift 5.5 you also get async throws. Prefer typed errors and exhaustive catch for recovery, and use Result when you need to store or pass the outcome.
8 What are lazy properties and property observers? Medium
A stored property can run code when it is set or read through observers and modifiers.
willSetanddidSetobserve changes to a stored property.willSetreceives the new value,didSetthe old, and neither fires during initialisation.lazydefers creation until first access. It is useful for expensive setup that depends on other properties, and the property must bevar.
class ViewModel {
lazy var formatter = DateFormatter()
var score = 0 {
didSet {
print("changed from \(oldValue) to \(score)")
}
}
}
Computed properties with get and set do not store a value and are recalculated on each access; a get-only computed property is read-only. Observers are not called when setting from within the initialiser or from a designated initialiser delegation.
Lazy properties are not thread-safe by themselves. Use observers for lightweight side effects, not heavy work.
9 How do async/await, tasks and actors support structured concurrency? Hard
Swift's structured concurrency builds on async/await, Task and TaskGroup. An async function can suspend without blocking a thread, and await marks a potential suspension point. Tasks form a hierarchy: a child is awaited and cancelled with its parent, which avoids leaked work.
func fetchAll(_ ids: [Int]) async throws -> [Item] {
try await withThrowingTaskGroup(of: Item.self) { group in
for id in ids {
group.addTask { try await fetch(id) }
}
var out: [Item] = []
for try await item in group { out.append(item) }
return out
}
}
An actor protects mutable state: its methods are mutually exclusive, so access is serialised. Cross-actor calls are async and require await. @MainActor marks code that must run on the main thread, and Sendable marks types safe to cross concurrency domains. Avoid Task.detached unless you truly want to escape structured cancellation.
10 How does copy-on-write work for Swift collections? Hard
Swift collections such as Array, Dictionary and String are value types with copy-on-write. Copying a variable is cheap because both copies share the same buffer until one mutates, then a unique copy is made.
var a = [1, 2, 3]
var b = a // shares storage, no copy yet
b.append(4) // copies here; a is still [1, 2, 3]
The runtime checks the reference count: if the buffer is uniquely referenced it mutates in place, otherwise it clones.
Pitfalls to know:
- A struct containing a class property does not deep-copy that reference when copied, so both copies share the class instance. Implement a custom copy or use value types instead.
- Building your own COW type requires
isKnownUniquelyReferencedand careful class-backed storage. - Passing a large array to a function is cheap until it is mutated.
- Sharing a value-type buffer across threads is not automatically safe unless access is synchronised.
Understanding COW matters for both performance and reasoning about shared state.
Frequently Asked Questions About Swift Interviews
What do hiring managers evaluate in Swift technical rounds?
Technical interviewers look for foundational fluency, idiomatic syntax, clarity when communicating complex logic, and awareness of performance trade-offs (e.g. memory footprint, render performance, and network latency) in production environments.
What are the best interview tips for practicing Swift questions?
Use active recall: summarize each answer in your own words before revealing the model solution. Focus on explaining why a certain approach is chosen rather than just memorizing code syntax.