The 60 Swift questions interviewers actually ask, with direct answers, runnable code, and what the interviewer is listening for. Grouped for freshers, intermediate, and experienced iOS rounds.
60 questions with answersKey Takeaways
Swift is a statically typed, compiled programming language Apple introduced at WWDC 2014 and open-sourced in 2015. It replaced Objective-C as the primary language for Apple platforms and now runs on Linux and Windows too, so the same language covers iOS apps and server backends. Its defining trait is safety: optionals force you to handle missing values, the type system is strict, and automatic reference counting (ARC) manages memory without a stop-the-world garbage collector. In interviews, Swift questions probe understanding of optionals, value versus reference semantics, memory management, and protocol-oriented design, not memorized keywords. This page collects the 60 questions that come up most, each with a direct answer and runnable code. Apple's official guide, The Swift Programming Language, is the canonical reference; increasingly the first Swift round is a live AI coding interview, so pair this bank with our AI interview preparation guides for the format side.
Watch: Swift Tutorial - Full Course for Beginners
Video: Swift Tutorial - Full Course for Beginners (freeCodeCamp.org, YouTube)
Test yourself and earn a certificate
10 quick questions. Score 70%+ to download your Swift certificate.
The fundamentals every entry-level round checks. If any answer here surprises you, that's your study list.
Swift is Apple's statically typed, compiled programming language, introduced in 2014 and open-sourced in 2015. It's the primary language for building iOS, macOS, watchOS, and tvOS apps, and it also runs on Linux and Windows for server-side and cross-platform code.
It was designed to be safer and more readable than Objective-C: optionals model missing values, the type system is strict, and ARC handles memory automatically. Beginners get productive quickly while it still powers production apps at scale.
Key point: A one-line definition plus its safety angle beats a memorized feature list. Interviewers open with this to hear how you organize an answer.
let declares a constant: once you assign a value, you can't reassign it. var declares a variable you can reassign. The Swift convention is to reach for let by default and switch to var only when you genuinely need mutation.
For value types, let also makes the whole value immutable, so a let array can't be appended to. For reference types, let fixes the reference but the object it points to can still change.
let pi = 3.14159
// pi = 3.14 // error: cannot assign to a constant
var counter = 0
counter += 1 // fine
let nums = [1, 2, 3]
// nums.append(4) // error: nums is immutableKey point: The follow-up is 'why prefer let?'. Answer: it makes intent clear and lets the compiler catch accidental mutation.
An optional is a type that either holds a value or holds nothing (nil). You write it with a trailing question mark: String? is either a String or nil. Swift uses optionals so the absence of a value is part of the type, not a hidden surprise.
The payoff is safety: the compiler won't let you use an optional as if it always has a value, so a whole class of null crashes disappears. You must unwrap an optional before using what's inside.
var name: String? = "Asha"
name = nil // allowed, it is optional
var age: Int = 30
// age = nil // error: Int is not optionalKey point: Optionals are the single most-tested Swift concept. Expect three or four follow-ups chaining off this one answer.
Watch a deeper explanation
Video: What are Optionals in Swift? (donny wals, YouTube)
The safe ways are optional binding (if let, guard let), the nil-coalescing operator (??) to supply a default, and optional chaining (?.) to keep working through a chain that might be nil. Force unwrapping with ! is the unsafe way: it crashes if the value is nil.
Prefer if let or guard let for control flow, ?? when you have a sensible default, and reserve ! for cases where nil truly can't happen and a crash would be the right outcome.
let input: String? = "42"
if let text = input {
print("got \(text)") // runs only when non-nil
}
let safe = input ?? "default" // nil-coalescing
let length = input?.count // optional chaining -> Int?Key point: Reciting all four unwrapping styles and when each fits is exactly what this question screens for.
guard checks a condition and, if it fails, must exit the current scope in its else block (return, throw, break, or continue). If the condition passes, execution continues and any values bound with guard let stay in scope afterward.
The difference from if is the shape it produces: guard handles the failure case up front and keeps the main logic unindented, so you avoid the pyramid of nested if let blocks. It's the standard tool for early exits and precondition checks.
func greet(_ name: String?) {
guard let name = name, !name.isEmpty else {
print("no name")
return
}
print("Hello, \(name)") // name is unwrapped and in scope here
}Key point: The bound value staying in scope after guard is the detail that shows you actually use it, not just recite it.
A struct is a value type: assigning it or passing it to a function copies it, so changes to the copy don't touch the original. A class is a reference type: assignment shares the same instance, so changes are visible everywhere that reference is held.
Structs get a memberwise initializer for free, don't support inheritance, and suit plain data. Classes support inheritance, identity (===), and deinitializers, and suit shared, mutable state. Apple's guidance is to prefer structs by default.
struct PointS { var x: Int }
class PointC { var x: Int; init(x: Int) { self.x = x } }
var a = PointS(x: 1); var b = a; b.x = 9
print(a.x) // 1 (copied)
let c = PointC(x: 1); let d = c; d.x = 9
print(c.x) // 9 (shared reference)| struct | class | |
|---|---|---|
| Semantics | Value (copied) | Reference (shared) |
| Inheritance | No | Yes |
| Memberwise init | Automatic | Manual |
| Deinit / identity === | No | Yes |
Key point: This is one of the two or three most-asked Swift questions. Have the copy-versus-share code example ready to draw.
Watch a deeper explanation
Video: Swift Structs - Value Type vs. Reference Type (Sean Allen, YouTube)
Value types (struct, enum, tuple) are copied on assignment and when passed around, so each variable owns an independent copy. Reference types (class, actor, closures) share a single instance, so multiple variables point to the same underlying object.
This drives real behavior: value types are easier to reason about and thread-safer because nobody else can mutate your copy, which is why Swift's standard library types like Array, String, and Dictionary are all structs.
Key point: Array and String are value types.
There are three: Array (ordered, allows duplicates), Set (unordered, unique elements, fast membership testing), and Dictionary (key-value pairs with fast lookup by key). All three are value types, so they're copied on assignment, and all three are generic, so an [Int] can't accidentally hold a String and the compiler enforces that.
Pick by need: Array when order matters, Set when you need uniqueness or O(1) contains checks, Dictionary when you look things up by key. Converting an array to a Set is a common way to dedupe or speed up membership tests.
let scores = [90, 85, 90]
let unique = Set(scores) // {90, 85}
let byName = ["Asha": 90, "Ben": 85]
print(byName["Asha"] ?? 0) // 90Type inference lets the compiler work out a variable's type from the value you assign, so let count = 5 is an Int without you writing it. You still get full static typing and its safety; you just skip the boilerplate.
You annotate the type explicitly when the compiler can't infer it, when you want a wider type (let value: Double = 3), or for clarity in an API. Inference is a convenience, not dynamic typing.
let name = "Swift" // inferred as String
let score = 95 // inferred as Int
let ratio = 0.5 // inferred as Double
let flag: Bool = true // explicit annotationOptional chaining lets you call properties, methods, and subscripts on an optional using ?. If any link in the chain is nil, the whole expression short-circuits and evaluates to nil instead of crashing the way a force unwrap would. Because any step can produce nil, the result of the chain is always itself an optional.
It replaces nested if let checks when you're just reaching through a chain that might not exist, like user?.address?.city, which returns a String? that's nil if user or address is nil.
struct Address { let city: String }
struct User { let address: Address? }
let user: User? = User(address: nil)
let city = user?.address?.city // String?, here nil
print(city ?? "unknown") // "unknown"The nil-coalescing operator returns the wrapped value of an optional when it's non-nil, and otherwise returns a default you supply on the right-hand side. So writing optional ?? fallback means 'use the value inside the optional, or this default if the optional is nil'. It's the shortest way to turn an optional into a definite value.
It's the cleanest way to turn an optional into a definite value at the point of use, and it short-circuits: the default expression only evaluates when the optional is nil.
let setting: Int? = nil
let timeout = setting ?? 30 // 30
let name: String? = "Asha"
print(name ?? "guest") // "Asha"A function uses func, a name, parameters with types, and an optional return type after ->. Each parameter has an argument label (used at the call site) and a parameter name (used inside the body); they default to the same word.
Argument labels make calls read like sentences: move(from: a, to: b). Use _ to drop a label when it adds no clarity. This label system is why Swift call sites read more clearly than most languages.
func greet(person name: String, from city: String) -> String {
return "Hello \(name) from \(city)"
}
greet(person: "Asha", from: "Pune")
func double(_ n: Int) -> Int { n * 2 } // no label
double(21) // 42A closure is a self-contained block of code you can pass around and call later, like an anonymous function. It can capture and store references to variables from the surrounding scope, which is where the name comes from.
Closures power callbacks, completion handlers, and functional methods like map and sorted. Swift has trailing-closure syntax and shorthand argument names ($0, $1) that make short closures compact.
let nums = [3, 1, 2]
let sorted = nums.sorted { $0 < $1 } // trailing closure, shorthand args
print(sorted) // [1, 2, 3]
let greet = { (name: String) in print("Hi \(name)") }
greet("Asha")Key point: Being able to write a trailing closure with $0 shorthand from memory is the fresher-level bar here.
Watch a deeper explanation
Video: Introduction to Swift: Closures (Paul Hudson, YouTube)
An enum defines a type with a fixed set of cases, like Direction.north. Unlike C-style enums, Swift enums are first-class types: they can have methods, computed properties, conform to protocols, and carry associated values so each case can store its own data.
That makes enums a modeling tool. A Result can be .success(value) or .failure(error), each carrying different data, and a switch over an enum is exhaustive, so the compiler flags a missing case.
enum Payment {
case cash
case card(last4: String)
}
let p = Payment.card(last4: "1234")
switch p {
case .cash: print("cash")
case .card(let last4): print("card ending \(last4)")
}The for-in loop iterates over any sequence: arrays, ranges, dictionaries, and strings. Ranges come in two forms: 1...5 is a closed range that includes 5, and 1..<5 is a half-open range that excludes 5. For condition-driven loops there's while, which checks before each pass, and repeat-while, which runs the body once before checking.
You can iterate with indices via enumerated() to get (index, element) pairs, and stride(from:to:by:) handles custom steps. There's no C-style for loop; ranges and sequences replace it.
for i in 1...3 { print(i) } // 1 2 3
for i in 0..<3 { print(i) } // 0 1 2
let names = ["Asha", "Ben"]
for (i, name) in names.enumerated() {
print("\(i): \(name)")
}String interpolation builds a string by embedding expressions directly inside a literal with the \(...) syntax. Anything you put inside the parentheses is evaluated and its result is inserted into the string, so you avoid clumsy manual concatenation with the plus operator. It reads cleanly and works with any expression, not just plain variables.
It works with any expression, not just variables, and Swift strings are Unicode-correct value types. For multi-line text, triple-quoted strings ("""...""") preserve line breaks.
let name = "Asha"
let score = 95
print("\(name) scored \(score), avg \(Double(score) / 2)")
// Asha scored 95, avg 47.5A stored property holds an actual value in memory as part of the instance. A computed property has no storage of its own; instead it runs a getter, and optionally a setter, every single time you access it, deriving its value from other properties on the fly. So a stored property remembers a value while a computed one recalculates it each time.
Use computed properties for values that should always stay in sync with their inputs, like an area derived from width and height, so there's no stale copy to keep updated.
struct Rectangle {
var width: Double
var height: Double
var area: Double { width * height } // computed, read-only
}
var r = Rectangle(width: 3, height: 4)
print(r.area) // 12
r.width = 5
print(r.area) // 20, always currentA protocol declares a set of requirements, methods, properties, or initializers, without implementing any of them. A type conforms by naming the protocol in its declaration and then providing everything the protocol requires. It's Swift's version of an interface, and one type can conform to many protocols at once, which is central to how Swift models shared behavior.
Protocols let unrelated types share a capability: anything conforming to Comparable can be sorted, anything conforming to your Drawable can be drawn. Protocol extensions can add default implementations, so conformers get behavior for free.
protocol Greetable {
var name: String { get }
func greet() -> String
}
struct Person: Greetable {
let name: String
func greet() -> String { "Hi, I am \(name)" }
}A function that can fail is marked throws and uses throw to raise an error (any type conforming to the Error protocol). Callers must handle it with do/catch, or convert it with try? (turns a failure into nil) or try! (crashes on failure).
Errors are usually enums that group related failures. The try keyword marks every call that can throw, so failure points are visible in the code rather than hidden.
enum LoginError: Error { case wrongPassword }
func login(_ pw: String) throws {
if pw != "secret" { throw LoginError.wrongPassword }
}
do {
try login("nope")
} catch {
print("failed: \(error)")
}as is a guaranteed cast the compiler already knows is safe (like a subclass to a superclass, or a literal to a wider type). as? is a conditional downcast that returns an optional, nil if the cast fails. as! force-casts and crashes if it fails.
The safe pattern is as? with if let or guard let, common when pulling a specific type out of an [Any] or checking a superclass reference against a subclass.
let items: [Any] = [1, "two", 3.0]
for item in items {
if let s = item as? String {
print("string: \(s)")
}
}Structs are value types, and by default their methods can't modify their own stored properties. Marking a method mutating tells Swift the method changes the value, so it's only callable on a var instance, not a let one.
This makes mutation explicit and is why calling a mutating method on a constant struct is a compile error. Classes don't need it because they're reference types; changing a property doesn't change the reference.
struct Counter {
var count = 0
mutating func bump() { count += 1 }
}
var c = Counter()
c.bump() // count is now 1
let fixed = Counter()
// fixed.bump() // error: cannot mutate a let structAny can hold an instance of absolutely any type at all: value types like Int and String, reference types, functions, and even optionals. AnyObject is narrower; it can hold only instances of class types, meaning reference types. So every AnyObject is an Any, but the reverse isn't true, and structs can't be stored in an AnyObject.
Reach for these sparingly. They erase type information, so you lose the compiler's help and usually have to cast back with as? before you can do anything useful. Generics are almost always the better tool when you want to stay flexible but keep type safety.
The ternary operator condition ? a : b evaluates the condition and returns a when true, b when false. It's a compact expression for a simple two-way choice, useful where an if statement would be too heavy, like inline in a return or an assignment.
Keep it for genuinely simple choices. Nesting ternaries hurts readability fast, and an if/else or a switch reads better once there's real logic in the branches.
let score = 72
let grade = score >= 60 ? "pass" : "fail"
print(grade) // "pass"A tuple groups several values into one compound value, and each element can be a different type: (Int, String) or a named form like (code: Int, message: String). Tuples are value types, so they're copied on assignment, and you can pull elements out by index or by name.
Use them for small, temporary groupings, like returning two values from a function, without defining a whole struct. Once a grouping is meaningful enough to name or reuse, promote it to a struct.
func minMax(_ nums: [Int]) -> (min: Int, max: Int) {
return (nums.min()!, nums.max()!)
}
let result = minMax([3, 1, 9, 4])
print(result.min, result.max) // 1 9Key point: The follow-up is 'tuple or struct?'. Answer: tuple for a quick local grouping, struct once it needs a name, methods, or reuse.
A Swift switch must be exhaustive: it has to cover every possible value or include a default, so the compiler flags a missing case at build time. There's no implicit fall-through between cases, so you don't need break, and one case can match multiple values or ranges.
It's also a full pattern-matching tool: cases can match ranges (0..<10), tuples, enum cases with associated values, and add where clauses for extra conditions. That makes switch far more expressive than a plain integer dispatch.
let point = (2, 0)
switch point {
case (0, 0): print("origin")
case (let x, 0): print("on x-axis at \(x)")
case (0, let y): print("on y-axis at \(y)")
case (let x, let y) where x == y: print("diagonal")
default: print("elsewhere")
}For candidates with working experience: language mechanics, memory, protocols, and the questions that separate users from understanders.
ARC tracks how many strong references currently point to each class instance. The compiler inserts the retain and release calls for you at compile time, and the moment an instance's reference count drops to zero, it's deallocated immediately and its deinit method runs. There's no background thread scanning for garbage; it all happens deterministically.
ARC applies only to reference types (classes), not value types, which are copied instead. It's not garbage collection: there's no background sweep and no pause, which is deterministic but means reference cycles won't get cleaned up on their own.
class File {
let name: String
init(name: String) { self.name = name }
deinit { print("\(name) freed") }
}
var a: File? = File(name: "log")
a = nil // count hits 0, prints "log freed"Key point: The near-guaranteed follow-up is 'so what happens with a reference cycle?'. Have the weak/unowned answer loaded and ready.
Watch a deeper explanation
Video: Swift - Retain Cycle, Automatic Reference Counting, Memory Leak - iOS Interview Questions (Sean Allen, YouTube)
A retain cycle happens when two objects hold strong references to each other, so neither's count ever reaches zero and both leak. The classic case is a parent holding a child and the child holding the parent back, or a closure capturing self while self holds the closure.
You break it by making one side of the relationship weak or unowned so it doesn't contribute to the reference count. Delegates are usually weak, and closures use a capture list like [weak self] to avoid holding their owner alive.
class ViewModel {
var onDone: (() -> Void)?
func load() {
onDone = { [weak self] in
self?.finish() // no cycle: closure does not retain self
}
}
func finish() {}
}Key point: Naming the two common cycles (delegate, closure capturing self) and their fixes is exactly what a mid-level round wants.
Both avoid increasing the reference count, so both break retain cycles. weak references are always optional and become nil automatically when the object is deallocated, so accessing them safely is fine. unowned references are non-optional and assume the object outlives the reference; accessing one after the object is gone crashes.
The rule of thumb: use weak when the other object can legitimately disappear first (a delegate), and unowned when the reference's lifetime is guaranteed to be inside the other object's lifetime (a child that never outlives its parent).
| weak | unowned | |
|---|---|---|
| Optional | Yes, becomes nil | No, non-optional |
| After deallocation | indicates nil, safe | Crash if accessed |
| Use when | Other object may vanish first | Other object outlives this reference |
Key point: The trap is defaulting to unowned for brevity. Choosing weak unless the lifetime is truly guaranteed is the safer, more production-ready answer.
By default a closure captures variables from its surrounding scope by reference, so it sees later changes to them and keeps any captured objects alive for as long as the closure exists. That's convenient, but it can create a retain cycle when a stored closure captures self and the same object also holds the closure, so neither can be freed.
A capture list, written in brackets before the parameters, lets you control capture: [weak self] captures self weakly, [unowned self] captures it unowned, and [value = someValue] captures a copy of the current value rather than a live reference.
var total = 0
let add = { total += 1 } // captures total by reference
add(); add()
print(total) // 2
var x = 10
let snapshot = { [x] in print(x) } // captures a copy
x = 99
snapshot() // 10, not 99Watch a deeper explanation
Video: Swift Closures Explained (Sean Allen, YouTube)
An escaping closure is one that outlives the function it's passed to: it's stored for later or called after the function returns, like a completion handler that runs when a network call finishes. You mark it @escaping in the parameter type.
Non-escaping is the default because it lets the compiler optimize and skip retain-cycle worries. Escaping closures can capture self and create cycles, which is why you often see [weak self] inside them.
var handlers: [() -> Void] = []
func store(_ handler: @escaping () -> Void) {
handlers.append(handler) // escapes: stored for later
}
func runNow(_ action: () -> Void) {
action() // non-escaping: called before return
}Generics let you write code that works with any type while keeping full type safety. A generic function or type uses a placeholder (like T) that the caller fills in, so one implementation serves Int, String, or your own types without duplication or casting.
The whole standard library is generic: Array<Element>, Dictionary<Key, Value>, Optional<Wrapped>. You can constrain a generic with where clauses or protocol bounds (T: Comparable) so it only accepts types that support the operations you need.
func firstIndex<T: Equatable>(of value: T, in array: [T]) -> Int? {
for (i, item) in array.enumerated() where item == value {
return i
}
return nil
}
firstIndex(of: "b", in: ["a", "b", "c"]) // 1Key point: Adding a protocol constraint (T: Equatable) in practice shows you understand generics as more than a wildcard.
A protocol extension adds methods and computed properties to a protocol itself, rather than to one concrete type. Any type that conforms to the protocol gets those implementations for free, so you write shared default behavior once in a single place instead of copying it into every conforming type. Conformers can still override a default where they need to differ.
This is the heart of protocol-oriented programming: define capability in a protocol, ship sensible defaults in its extension, and let types override only where they differ. It favors composition over class inheritance.
protocol Named { var name: String { get } }
extension Named {
func label() -> String { "Name: \(name)" } // default for all conformers
}
struct City: Named { let name: String }
City(name: "Pune").label() // "Name: Pune"Protocol-oriented programming models behavior with protocols and protocol extensions instead of a class hierarchy. A type composes several protocols, each one contributing a capability along with default implementations from its extension, rather than inheriting everything from a single superclass. You build up behavior by adopting small focused protocols instead of extending one deep base class.
The advantages: value types (structs) can participate, a type can adopt many protocols where it can only have one superclass, and you avoid deep, brittle inheritance chains. Apple has pushed this style since the 'Protocol-Oriented Programming in Swift' talk.
Key point: Saying 'structs can't inherit but can conform to many protocols' cleanly explains why this style exists in Swift.
Associated values let each enum case store its own related data of any type. So a case isn't just a bare label; it can carry a payload attached to it, and different cases in the same enum can carry completely different payloads. That turns an enum into a way to model a value that's exactly one of several shapes, each shape holding the data that fits it.
This makes enums a modeling tool for states that come with data: a Result is .success(T) or .failure(Error), a network response is .loading, .loaded(data), or .failed(message). You extract the values by pattern matching in a switch or if case.
enum Barcode {
case upc(Int, Int, Int, Int)
case qr(String)
}
let code = Barcode.qr("HELLO")
switch code {
case .upc(let a, let b, let c, let d): print(a, b, c, d)
case .qr(let text): print(text) // "HELLO"
}map transforms every element and returns a new collection of results. filter keeps only elements that pass a test. reduce combines all elements into a single value using a running accumulator. All three take a closure and don't mutate the original.
compactMap is the common fourth: it maps and drops any nil results in one pass, handy when your transform returns optionals. flatMap flattens nested collections.
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
["1", "x", "3"].compactMap { Int($0) } // [1, 3]A lazy property isn't computed until the first time it's accessed. You mark it lazy var, and its initializer runs once, on first use, then the result is stored. It must be var because it's set after the instance is created.
Use it when the initial value is expensive to build or depends on other properties that must exist first, and you might not always need it. The cost is that lazy properties aren't thread-safe on first access.
class Report {
lazy var data: [Int] = {
print("building") // runs only on first access
return Array(1...1000)
}()
}
let r = Report() // nothing printed yet
r.data // "building" prints nowProperty observers run code when a stored property's value changes. willSet fires just before the new value is stored, giving you newValue, and didSet fires right after the change, giving you oldValue so you can compare. Neither observer runs during initialization, only on later assignments, which is a detail people forget.
Use them for side effects tied to a value change: updating a UI label when a model property changes, validating a bound, or logging. They keep that reactive logic next to the property instead of scattered across setters.
class Thermostat {
var temperature: Int = 20 {
didSet {
print("changed from \(oldValue) to \(temperature)")
}
}
}
Thermostat().temperature = 25 // changed from 20 to 25An extension adds functionality to an existing type, even one you don't own like Int or String from the standard library. Extensions can add computed properties, methods, initializers, subscripts, and protocol conformance to that type. What they can't do is add stored properties or override an existing method, since they only extend rather than replace.
They're a clean way to organize code (group a type's protocol conformance in its own extension) and to add small helpers to standard types without subclassing. Conforming a type to a protocol in an extension is a very common pattern.
extension Int {
var isEven: Bool { self % 2 == 0 }
func times(_ action: () -> Void) {
for _ in 0..<self { action() }
}
}
4.isEven // true
3.times { print("hi") }Codable is a type alias for Encodable and Decodable. Marking a type Codable lets Swift automatically convert it to and from formats like JSON, as long as all its properties are also Codable. JSONEncoder and JSONDecoder do the work.
When JSON keys don't match your property names, a CodingKeys enum maps them. For custom logic you can implement init(from:) and encode(to:) yourself, but the automatic synthesis covers most models.
struct User: Codable {
let id: Int
let name: String
}
let json = #"{"id": 1, "name": "Asha"}"#.data(using: .utf8)!
let user = try JSONDecoder().decode(User.self, from: json)
print(user.name) // "Asha"defer schedules a block of code to run when the current scope exits, no matter how it exits, whether by returning normally, throwing an error, or breaking out of a loop. If you write several defer blocks in one scope, they run in reverse order of declaration, so the last one you wrote runs first. The block always runs, which is the point.
It's the tool for cleanup that must always happen: closing a file, releasing a lock, undoing a temporary change. Putting the cleanup right next to the setup keeps the two together even when there are multiple exit points.
func process() {
print("open")
defer { print("close") } // runs last, on any exit
print("work")
}
process() // open, work, closesorted(by:) returns a new sorted array and sort(by:) sorts in place, both taking a closure that returns true when the first argument should come before the second. For types that are Comparable, plain sorted() works with no closure.
As of Swift 5, the standard library sort is guaranteed stable: elements that compare equal keep their original relative order. That lets you sort by a secondary key, then a primary key, and get a correct multi-level ordering.
var people = [("Ben", 30), ("Asha", 30), ("Cara", 25)]
people.sort { $0.1 < $1.1 } // by age; equal ages keep prior order
print(people)
// [("Cara", 25), ("Ben", 30), ("Asha", 30)]some Protocol is an opaque return type: it says 'this returns one specific concrete type that conforms to Protocol, and it's the same type every time, but I'm not telling the caller which one'. The caller knows the capabilities, not the exact type.
SwiftUI uses it everywhere (some View) because view bodies return one complex concrete type you don't want to spell out. It differs from returning the protocol itself, which allows different types and loses some compiler optimizations.
func makeAdder() -> some Sequence {
return [1, 2, 3] // concrete type Array<Int>, hidden behind some Sequence
}A subscript lets you access elements of a type with bracket syntax, like array[0] or dictionary[key]. You define one with the subscript keyword, giving it parameters and a return type, plus an optional setter so the bracket access can be assigned to. It's how a type exposes collection-style access instead of forcing callers to call a named method.
You add subscripts to your own types to give them collection-like access, a matrix indexed by row and column, or a wrapper that exposes its contents by key. They can be overloaded to take different parameter types.
struct Grid {
var data = Array(repeating: 0, count: 9)
subscript(row: Int, col: Int) -> Int {
get { data[row * 3 + col] }
set { data[row * 3 + col] = newValue }
}
}
var g = Grid()
g[1, 1] = 5
print(g[1, 1]) // 5From most to least restrictive: private (the enclosing declaration and its extensions in the same file), fileprivate (the whole file), internal (the whole module, the default), public (other modules can use it but not subclass or override), and open (other modules can also subclass and override).
The distinction between public and open matters for library authors: public exposes an API for use, open additionally allows external subclassing. Default internal means you only annotate when you want to widen or narrow visibility.
Result<Success, Failure> is a standard-library enum with two cases, .success(value) and .failure(error), where the error type conforms to Error. It packages the outcome of an operation that can fail into a single value you can store, pass, and inspect later.
It shines in asynchronous callbacks, where you can't use throw across a closure boundary: a completion handler takes a Result and the caller switches over it. You can also convert it back to throwing code with try result.get().
func fetch(completion: (Result<Int, Error>) -> Void) {
completion(.success(42))
}
fetch { result in
switch result {
case .success(let value): print(value)
case .failure(let error): print(error)
}
}advanced rounds probe internals, concurrency, design judgment, and production scars. Expect every answer here to draw a follow-up.
async marks a function that can suspend; await marks the point where it might. When an async function awaits, it gives up its thread so other work can run, then resumes when the awaited operation completes. This replaces nested completion handlers with straight-line code.
Structured concurrency ties child tasks to a parent scope: async let and TaskGroup spawn concurrent work whose lifetime is bounded by the enclosing function, so cancellation and errors propagate predictably and nothing leaks past the scope.
func loadProfile() async throws -> Profile {
async let user = fetchUser() // starts concurrently
async let posts = fetchPosts()
return try await Profile(user: user, posts: posts)
}Key point: The follow-up is usually 'how is this different from GCD?'. Answer: it's cooperative, structured, and the compiler enforces sendability.
An actor is a reference type that protects its mutable state from data races. Only one task can access an actor's isolated state at a time; the compiler routes external access through await and serializes it, so you can't accidentally read and write shared state from two threads at once.
Actors replace manual locks and serial queues for state isolation. The main-thread version is the @MainActor annotation, which guarantees UI-touching code runs on the main thread. The trade-off is that cross-actor calls are async.
actor Counter {
private var value = 0
func increment() { value += 1 }
func current() -> Int { value }
}
let c = Counter()
await c.increment()
print(await c.current())Key point: Naming reentrancy (an actor can suspend mid-method and let other calls in) is the detail that marks real concurrency experience.
Sendable marks a type as safe to pass across concurrency boundaries (between tasks or actors). Value types with Sendable members are Sendable automatically; classes must be immutable or internally synchronized to qualify. The compiler checks Sendable conformance to flag potential data races at compile time.
This is Swift's strict concurrency checking: when you send a non-Sendable value into a different isolation domain, the compiler warns or errors, turning a class of runtime races into build failures. @unchecked Sendable is the escape hatch when you've synchronized manually.
Swift's standard collections (Array, Dictionary, Set, String) are value types, but copying one doesn't duplicate its storage immediately. The copy shares the same underlying buffer until one side mutates it; only then is a real copy made. That keeps value semantics cheap.
The check uses isKnownUniquelyReferenced on the internal storage: if the buffer has one owner, mutate in place; if it's shared, copy first. You can implement the same pattern for your own value types that wrap a class-backed buffer.
var a = [1, 2, 3]
var b = a // shares storage, no copy yet
b.append(4) // b is now unique-ish, storage copied here
print(a) // [1, 2, 3], unchangedKey point: Explaining that the copy is deferred until mutation, not skipped, is what separates a real answer from 'arrays are copied'.
Structs live on the stack when possible and avoid ARC's retain/release traffic, so small, frequently passed value types are often faster and easier to reason about. Apple's guidance is to default to structs and reach for classes only when you need identity, inheritance, or shared mutable state.
The trade-offs cut both ways: large structs are expensive to copy if copy-on-write doesn't apply, and structs holding class references still incur ARC on those references. Deeply nested value types can also stress the compiler and runtime, so measure rather than assume.
Relative cost of common per-access operations
Approximate order-of-magnitude tiers, not measured benchmarks. Use to reason about where cost concentrates, not for exact numbers.
A generic constraint (func f<T: Shape>(_ x: T)) resolves to one concrete type at each call site, so the compiler can specialize and inline. An existential (any Shape) is a boxed value that can hold any conforming type, decided at runtime, which costs indirection and dynamic dispatch.
Swift now makes existentials explicit with the any keyword precisely because they're not free. Prefer generics or opaque types (some) when the concrete type is known at compile time; use any only when you genuinely need to store a heterogeneous mix behind one protocol.
Key point: Knowing why any was made explicit (to surface the runtime cost) is a strong signal of keeping up with the language.
A property wrapper is a type that adds behavior to a property by wrapping its storage. You mark it @propertyWrapper and give it a wrappedValue; then any property annotated with your wrapper's name routes get and set through it. The compiler generates the backing storage.
They factor out repeated property logic: clamping to a range, thread-safe access, persistence to UserDefaults. SwiftUI's @State, @Published, and @Environment are all property wrappers, which is why they behave differently from plain properties.
@propertyWrapper
struct Clamped {
private var value: Int
private let range: ClosedRange<Int>
init(wrappedValue: Int, _ range: ClosedRange<Int>) {
self.range = range
self.value = min(max(wrappedValue, range.lowerBound), range.upperBound)
}
var wrappedValue: Int {
get { value }
set { value = min(max(newValue, range.lowerBound), range.upperBound) }
}
}
struct Player { @Clamped(0...100) var health = 120 }
Player().health // 100Swift uses several dispatch mechanisms. Value types and final methods use static dispatch, resolved at compile time and inlinable. Non-final class methods use dynamic dispatch through a vtable. Protocol requirements use a witness table. Methods marked @objc or dynamic use Objective-C message dispatch, the slowest but most flexible.
It matters for performance and behavior: marking a class or method final enables static dispatch and inlining, while protocol-extension methods that aren't protocol requirements dispatch statically, which produces a well-known gotcha where the wrong implementation runs.
Key point: The protocol-extension dispatch gotcha (a non-requirement method calls the extension's version, not the type's override) is a favorite senior follow-up.
If a method is declared in the protocol itself, calls go through the witness table and dynamic dispatch picks the conforming type's version. If a method exists only in the protocol extension (not listed as a protocol requirement), it's dispatched statically based on the variable's declared type, so a type's own implementation can be bypassed.
The fix is to add the method to the protocol's requirement list, which forces dynamic dispatch. This trips people up when they store an object as its protocol type and see the extension's default run instead of the concrete override.
protocol Greeter { func hi() } // requirement -> dynamic
extension Greeter {
func hi() { print("default") }
func bye() { print("ext bye") } // not a requirement -> static
}
struct En: Greeter {
func hi() { print("hello") }
func bye() { print("goodbye") }
}
let g: Greeter = En()
g.hi() // "hello" (dynamic)
g.bye() // "ext bye" (static, the gotcha)Swift enforces that a variable can't be accessed in conflicting ways at the same time: you can't have two simultaneous writes, or a write overlapping a read, to the same storage. The compiler checks most of this statically, and the runtime catches the rest, trapping on a violation.
It surfaces with inout parameters (passing the same variable to two inout slots is an error) and with mutating methods that call back into the same value. The rule is what makes value semantics and safe optimization possible.
func swapInts(_ a: inout Int, _ b: inout Int) {
let t = a; a = b; b = t
}
var x = 1
// swapInts(&x, &x) // error: overlapping access to xPlain throws propagates any Error; the caller doesn't know the concrete type without inspecting it. rethrows applies to functions that only throw because a closure argument throws, so they're non-throwing when passed a non-throwing closure. Typed throws (throws(MyError)) constrains a function to one error type, useful in constrained or embedded contexts.
Result reifies the outcome into a value, which suits async callbacks and storing outcomes, while throws suits synchronous straight-line code. Modern async code mostly uses async throws and lets structured concurrency propagate errors.
You overload an existing operator by writing a static func with the operator as its name inside a type, or a free function. For a brand-new operator symbol you first declare it with operator plus its fixity (prefix, infix, postfix) and, for infix, a precedence group.
Use overloading for types where an operator reads naturally (adding two vectors with +). Be sparing with entirely new operators: unfamiliar symbols hurt readability, and most teams prefer named methods unless the domain strongly implies an operator.
struct Vector { var x, y: Double }
extension Vector {
static func + (l: Vector, r: Vector) -> Vector {
Vector(x: l.x + r.x, y: l.y + r.y)
}
}
Vector(x: 1, y: 2) + Vector(x: 3, y: 4) // (4, 6)Wrap the leaf callbacks first with continuations: withCheckedContinuation for non-throwing APIs and withCheckedThrowingContinuation for throwing ones, resuming the continuation exactly once inside the old completion handler. That gives you async entry points without rewriting the call chain at once.
Then work outward, replacing callback signatures with async throws and folding nested handlers into straight-line awaits. Adopt strict concurrency checking gradually per module, fix Sendable warnings at the boundaries, and keep UI updates on @MainActor.
func fetchAsync() async throws -> Data {
try await withCheckedThrowingContinuation { cont in
oldFetch { data, error in
if let error { cont.resume(throwing: error) }
else { cont.resume(returning: data!) }
}
}
}Key point: Naming 'resume exactly once' matters: resuming a continuation twice or never is a classic bug this question probes.
Start from the signal: symbolicate the crash reports (Xcode Organizer, or a service like Crashlytics) to find the faulting frame, and check whether it correlates with a recent release or device/OS. For force-unwrap and array-bounds crashes, the stack usually points straight at the line.
For leaks and cycles, use Instruments: the Leaks and Allocations instruments to watch growth, and the memory graph debugger to find retain cycles (often a closure capturing self or a strong delegate). Then fix at the right layer, weak capture, break the cycle, add a bound, and confirm with the same tool.
Key point: The structure is: observe, isolate, fix, confirm.
Clarify first: eviction policy (LRU, size or count bound), concurrency needs, and whether entries expire. Then pick isolation: an actor is the modern choice, since it serializes access to the backing dictionary without manual locks and composes with async callers. For a synchronous API, a serial DispatchQueue or an NSLock around the dictionary works.
For eviction, back an LRU with a dictionary plus a doubly linked list so both lookup and recency updates stay O(1). The operational edges: memory pressure (respond to didReceiveMemoryWarning or use NSCache which evicts under pressure automatically), and thundering-herd on cache misses matters.
actor Cache<Key: Hashable, Value> {
private var store: [Key: Value] = [:]
func get(_ key: Key) -> Value? { store[key] }
func set(_ key: Key, _ value: Value) { store[key] = value }
}
let cache = Cache<String, Int>()
await cache.set("a", 1)
print(await cache.get("a") ?? -1) // 1Swift wins when you want type safety and modern syntax on Apple platforms without the ceremony of Objective-C. It trades Objective-C's dynamic runtime and mature tooling history for a stricter compiler that catches nil and type mistakes early. Against Kotlin, the story is close: both are modern, null-safe, statically typed languages, but Swift targets Apple's ecosystem first while Kotlin targets Android and the JVM. Java sits further away, older and more verbose, with null handled by convention rather than the type system. Saying these trade-offs out loud is itself an interview signal: it shows you pick tools on merits, not habit.
| Language | Null handling | Best at | Watch out for |
|---|---|---|---|
| Swift | Optionals in the type system | iOS, macOS, Apple platforms, server-side | Smaller ecosystem off Apple platforms |
| Objective-C | nil messaging, no compiler check | Legacy Apple codebases, C interop | Verbose syntax, weaker safety |
| Kotlin | Nullable types in the type system | Android, JVM, multiplatform | Apple support via KMP is still maturing |
| Java | Null by convention, no check | Enterprise backends, Android (older) | Verbose, null pointer exceptions |
Prepare in layers, and practice out loud. Most Swift rounds move from concept questions to live coding to a design or debugging discussion, so rehearse each stage rather than only reading answers.
The typical Swift interview flow
Earlier rounds increasingly run as AI coding interviews. The concepts on this page are what gets probed at every stage.
10 questions, about 6 minutes. Score 70% or higher to earn a shareable certificate.
Hyring builds the AI Coding Interviewer that runs live technical rounds in 20+ languages for 5,000+ hiring teams. These Swift questions and scoring notes reflect what actually gets asked and evaluated inside the interviews we host.
See how the AI Coding Interviewer works