Swift Interview Questions and Answers
25 hand-picked Swift interview questions with
detailed answers. Open the interactive version above to search, filter
by difficulty, run code, bookmark questions and track your progress.
What is Swift, and why is it used for iOS development?
Swift is Apple's modern, open-source programming language used to develop applications for iOS, macOS, watchOS, and tvOS. It is designed to be fast, safe, and expressive, offering features like type inference, optionals, automatic memory management (ARC), and strong compile-time safety.
What is the difference between var and let in Swift?
var declares a mutable variable whose value can change after initialization.
let declares an immutable constant whose value cannot be changed once assigned.
var age = 25
age = 26
let name = "John"
// name = "Mike" // Error
What are Optionals in Swift?
An Optional represents a value that may either contain a value or be nil. Swift uses optionals to prevent null pointer exceptions by forcing developers to safely unwrap values before using them.
Common unwrapping techniques include optional binding (if let, guard let) and the nil-coalescing operator (??).
var username: String?
if let name = username {
print(name)
}
What is ARC (Automatic Reference Counting) in Swift?
ARC (Automatic Reference Counting) automatically manages memory by keeping track of object references. When an object's reference count becomes zero, Swift automatically deallocates it.
To avoid retain cycles, Swift provides weak and unowned references.
What is the difference between a Struct and a Class in Swift?
Structs are value types. They are copied when assigned or passed to functions.
Classes are reference types. Multiple variables can reference the same object instance.
Classes support inheritance, deinitializers, and reference counting, while structs do not.
struct User {
var name: String
}
class Person {
var name = "John"
}
guard let vs if let for unwrapping optionals?
if let binds the unwrapped value only inside the if block. guard let binds it for the rest of the enclosing scope and requires an else that exits (return/break/throw).
guard enables an early-exit style that avoids the nested 'pyramid of doom' and keeps the happy path un-indented.
func greet(_ name: String?) {
guard let name = name else { return }
print("Hello, \(name)") // name is non-optional here
}
What are closures in Swift?
Self-contained blocks of code that can be passed around and capture references to variables from their surrounding context. They power callbacks, completion handlers, and higher-order functions.
Swift adds sugar: shorthand argument names ($0), implicit return, and trailing closure syntax.
let add = { (a: Int, b: Int) in a + b }
let sorted = [3, 1, 2].sorted { $0 < $1 }
Enums and associated values.
An enum models a type with a fixed set of cases. Swift enums are powerful: each case can carry associated values, can have raw values, and the enum can define computed properties and methods.
Ideal for representing state and outcomes exhaustively — the compiler forces you to handle every case in a switch.
enum Loadable {
case idle
case loading
case success(String)
case failure(Error)
}
What is a protocol in Swift?
A protocol is a blueprint of methods and properties a conforming type must provide — Swift's version of an interface. Structs, classes, and enums can all conform, enabling polymorphism without class inheritance.
Protocol extensions can supply default implementations, so conformers get behavior for free.
protocol Drivable {
func drive()
}
extension Drivable {
func drive() { print("Vroom") } // default
}
Array vs Set vs Dictionary.
- Array — ordered, index-accessed, allows duplicates.
- Set — unordered, unique elements, O(1) membership tests and set algebra (union/intersection).
- Dictionary — unordered key → value pairs, O(1) lookup by key.
All three are value types (copy semantics) with copy-on-write storage.
var nums = [1, 2, 2, 3]
let unique = Set(nums) // {1, 2, 3}
var ages = ["Ann": 30, "Bo": 25]
map, filter, reduce, and compactMap.
- map — transforms every element.
- filter — keeps elements matching a condition.
- reduce — combines all elements into a single value.
- compactMap — maps, then drops the
nil results (unwrapping optionals). - flatMap — maps and flattens nested sequences.
let nums = [1, 2, 3, 4]
let doubled = nums.map { $0 * 2 } // [2,4,6,8]
let evens = nums.filter { $0 % 2 == 0 } // [2,4]
let sum = nums.reduce(0, +) // 10
Error handling with do / try / catch.
A function that can fail is marked throws; callers use try inside a do/catch. Variants: try? converts the throw to an optional (nil on error), try! force-unwraps and crashes on error.
Errors are just values conforming to the Error protocol — typically enums.
enum NetError: Error { case offline }
func load() throws -> Data { throw NetError.offline }
do {
let data = try load()
} catch NetError.offline {
print("No connection")
}
weak vs unowned references.
Both break retain cycles by not incrementing the reference count.
- weak — always optional, automatically becomes
nil when the referent is deallocated. Use when the reference can outlive the object (e.g. a delegate). - unowned — non-optional, assumes the referent always outlives it; accessing it after deallocation crashes. Use only when the lifetimes are guaranteed.
class ViewModel {
var onDone: (() -> Void)?
func setup() {
onDone = { [weak self] in self?.finish() }
}
func finish() {}
}
Generics in Swift.
Generics let you write reusable, type-safe code parameterized over types (<T>) instead of duplicating logic or falling back to Any. Type constraints (T: Comparable) restrict what the type must support.
The standard library is built on them — Array<Element>, Optional<Wrapped>, Dictionary<Key, Value>.
func maxOf<T: Comparable>(_ a: T, _ b: T) -> T {
a > b ? a : b
}
What are extensions?
Extensions add functionality — methods, computed properties, initializers, nested types, and protocol conformances — to an existing type without subclassing, even to types you don't own (like String or Int).
Limitation: they cannot add stored properties (no new storage), only computed ones.
extension String {
var isBlank: Bool {
trimmingCharacters(in: .whitespaces).isEmpty
}
}
What is protocol-oriented programming (POP)?
Apple's recommended paradigm: prefer protocols + protocol extensions over class inheritance. Define behavior in protocols, provide defaults in extensions, and let value types (structs) conform.
It favors composition over inheritance, works with value semantics, and sidesteps the fragile-base-class and single-inheritance limits of OOP.
protocol Identifiable { var id: String { get } }
protocol Timestamped { var created: Date { get } }
struct Post: Identifiable, Timestamped {
let id: String
let created: Date
}
What is copy-on-write (COW)?
Swift's standard collections (Array, Dictionary, Set, String) share underlying storage when copied and only make a real copy the moment one copy is mutated. So passing arrays around is cheap — you pay for the copy only on write.
var a = [1, 2, 3]
var b = a // no copy yet — shared buffer
b.append(4) // NOW b copies; a stays [1,2,3]
Codable — JSON encoding and decoding.
Codable = Encodable & Decodable. Conform a type and Swift synthesizes the (de)serialization; JSONEncoder/JSONDecoder do the work. Use a CodingKeys enum to map JSON field names to Swift property names.
struct User: Codable {
let id: Int
let fullName: String
enum CodingKeys: String, CodingKey {
case id, fullName = "full_name"
}
}
let u = try JSONDecoder().decode(User.self, from: data)
What does defer do?
A defer block schedules code to run when the current scope exits — however it exits (normal fall-through, return, or a thrown error). Perfect for guaranteed cleanup: closing files, releasing locks, ending a context.
Multiple defers execute in reverse (LIFO) order.
func process(_ file: File) throws {
file.open()
defer { file.close() } // runs on every exit path
try file.read()
}
Stored vs computed properties, lazy, and observers.
- Stored — holds a value in memory.
- Computed — no storage; a
get (and optional set) calculates on each access. - lazy — a stored property initialized only on first access (defer expensive setup).
- Observers —
willSet/didSet run around a stored property's changes.
class Report {
lazy var data = expensiveLoad() // runs once, on demand
var title = "" { didSet { print("changed") } }
var isEmpty: Bool { title.isEmpty } // computed
}
Access control levels in Swift.
From most to least open:
- open — usable and subclassable/overridable across modules.
- public — usable across modules, but not subclassable outside.
- internal — default; visible within the same module.
- fileprivate — within the same file.
- private — within the enclosing declaration (and its extensions in the same file).
public struct API {
private var token: String
public init(token: String) { self.token = token }
}
How does async/await work in Swift?
Swift's structured concurrency: mark a function async, call it with await at a suspension point. A Task starts async work from a sync context, and async let runs children in parallel.
It replaces nested completion handlers with linear, readable code — and the compiler enforces safety around it.
func loadProfile() async throws -> Profile {
async let user = fetchUser() // parallel
async let posts = fetchPosts()
return Profile(user: try await user, posts: try await posts)
}
What are actors in Swift?
Actors are reference types that protect their mutable state from data races by serializing access — only one task touches an actor's state at a time. Reaching in from outside requires await (an actor 'hop').
@MainActor is a special actor pinning work to the main thread — essential for UI updates.
actor Counter {
private var value = 0
func increment() { value += 1 }
func current() -> Int { value }
}
let c = Counter()
await c.increment()
The Result type — what and when?
Result<Success, Failure: Error> is an enum with .success(value) and .failure(error) — it models an outcome as a value you can store and pass around. Common in completion-handler APIs (pre-async), and for capturing an error to handle later.
.get() throws, and map/flatMap transform the success case.
func fetch(completion: (Result<Data, Error>) -> Void) { }
fetch { result in
switch result {
case .success(let data): print(data)
case .failure(let err): print(err)
}
}
some vs any (opaque vs existential types).
some P — an opaque type: the compiler knows the single concrete type, it's just hidden from the caller. No runtime boxing, better performance (SwiftUI's some View).
any P — an existential: holds any conforming type, boxed at runtime. More flexible (heterogeneous values) but with indirection/overhead.
func makeShape() -> some Shape { Circle() } // opaque, one type
let shapes: [any Shape] = [Circle(), Square()] // existential, mixed