Swift Interview Idioms: The Shortcuts That Actually Save Time

May 29, 202610 min read
dsaalgorithmsinterview-prepswift
Swift Interview Idioms: The Shortcuts That Actually Save Time
TL;DR
  • Swift has no built-in heap — memorize a compact generic struct with push, pop, and a comparator closure before your interview.
  • Swift strings can't be integer-indexed — convert to Array(s) at the start of every string problem to avoid fighting the type system.
  • freq[c, default: 0] += 1 is the idiomatic frequency counter; the subscript with default writes back in one pass, no Optional unwrapping.
  • ArraySlice indices are not zero-basedarr[2...5][0] crashes; use Array(slice) or iterate with slice.indices to reset.
  • stride(from:through:by:) replaces C-style reverse loops; use it whenever you need reverse iteration or a non-unit step.
  • compactMap maps and filters nils in one pass, cleaner than map followed by filter { $0 != nil }.
  • Integer overflow panics in debug mode — guard large sums and BFS distances; use &+ for intentional wrapping.

Swift is a reasonable choice for a coding interview, and most Apple-focused engineers already think in it. The type system catches a whole class of bugs before you run a single line. The standard library is expressive. The compiler error messages are genuinely helpful, which is rare enough to be worth mentioning.

The problem? Swift's standard library is simultaneously very good and weirdly incomplete. The gaps show up at exactly the wrong moments, like when you are twelve minutes into a 45-minute interview and you just realized there is no heap.

Three Collections Cover 90% of What You Need

Array is the default. Appending is amortized O(1), random access is O(1), and almost everything in the standard library is designed around it. Initialize with a repeated value using Array(repeating: 0, count: n), which you will use constantly for DP tables, frequency counts, and visited arrays.

var dp = Array(repeating: Int.min, count: n + 1) var grid = Array(repeating: Array(repeating: false, count: cols), count: rows)

Dictionary is O(1) average for get, set, and delete. The idiom you want for frequency counting is the subscript with a default value:

var freq: [Character: Int] = [:] for c in s { freq[c, default: 0] += 1 }

Without the default:, you get an Optional back and the code gets ugly fast. The default: form returns a non-optional and writes back through the subscript in one shot. Use it every time.

Set gives O(1) membership testing and three algebraic operations that earn their keep in graph and interval problems. The algebraic ops are worth knowing because a.intersection(b) reads better than a manual loop, and in an interview you want to be explaining your algorithm, not narrating collection mechanics.

let a: Set = [1, 2, 3] let b: Set = [2, 3, 4] a.union(b) // [1, 2, 3, 4] a.intersection(b) // [2, 3] a.subtracting(b) // [1]

Sorting Without Surprises

Swift's sort is stable as of Swift 5, which means equal elements keep their original relative order. This matters for problems that sort by one key and then need to preserve secondary ordering. It also means you can stop mentally checking whether your comparator needs to handle equal elements carefully. One less thing to get wrong under pressure.

sort() mutates the array in place and requires var. sorted() returns a new array and works on let. Pick based on whether you need the original.

var nums = [3, 1, 4, 1, 5] nums.sort() // mutates: [1, 1, 3, 4, 5] let sorted = nums.sorted() // returns copy, nums unchanged let desc = nums.sorted(by: >) // descending

For custom comparators, the closure receives two elements and returns true if the first should come before the second:

pairs.sort { a, b in if a.1 != b.1 { return a.1 > b.1 } return a.0 < b.0 }

One trap: sort(by:) requires strict weak ordering. Returning true for equal elements causes undefined behavior. Use < and >, never <= or >=, as your primary comparators. This rule is not negotiable and the resulting bug is not reproducible on demand, which is the worst kind.

Why String Indexing Fights You

You cannot subscript a Swift String with an integer index. The reason is correct (Swift strings are Unicode-aware and characters can be multi-byte, so O(1) random access isn't possible), but it means you will fight the type system on every string problem at exactly the moment you want to be thinking about the algorithm.

The fastest fix: convert to an array of Character at the start and never look back.

let s = "hello" var chars = Array(s) // [Character] // Now you can do chars[i], chars[i] = "x", etc.

Do this reflexively. Don't try to use String.Index in an interview unless the problem specifically requires it. The cognitive overhead isn't worth it.

When you need the ASCII value of a character, asciiValue returns UInt8?, not Int. You need an explicit cast when doing arithmetic, which is another small tax the language collects from you:

let c: Character = "A" let val = Int(c.asciiValue!) // 65 let offset = Int(c.asciiValue!) - Int(Character("a").asciiValue!)

For checking character type, use the built-in properties instead of ASCII arithmetic when you can: c.isLetter, c.isNumber, c.isWhitespace, c.isUppercase, c.isLowercase.

Building a result string back from characters: String(chars) works. String concatenation in a loop is O(n²), same as every other language. Same lesson, different syntax.

Angry cat smashing laptop: "When the client says the bug only happens sometimes", runtime crash in Swift from ArraySlice index bug

ArraySlice[0] in production.

The Heap Is Missing. Here Is One.

Swift's standard library has no heap and no priority queue. This is the gap that bites Swift interviewees the hardest, because heap problems show up often and there is no workaround that doesn't require writing your own.

Apple's swift-collections package includes a Heap type, but most online judges running Swift will not have it available. Write this from memory:

struct Heap<T> { var data: [T] let isHigherPriority: (T, T) -> Bool init(_ compare: @escaping (T, T) -> Bool) { self.data = [] self.isHigherPriority = compare } var isEmpty: Bool { data.isEmpty } var count: Int { data.count } var peek: T? { data.first } mutating func push(_ val: T) { data.append(val) siftUp(data.count - 1) } mutating func pop() -> T? { guard !data.isEmpty else { return nil } data.swapAt(0, data.count - 1) let val = data.removeLast() if !data.isEmpty { siftDown(0) } return val } private mutating func siftUp(_ i: Int) { var i = i while i > 0 { let parent = (i - 1) / 2 if isHigherPriority(data[i], data[parent]) { data.swapAt(i, parent) i = parent } else { break } } } private mutating func siftDown(_ i: Int) { var i = i while true { var best = i let l = 2 * i + 1, r = 2 * i + 2 if l < data.count && isHigherPriority(data[l], data[best]) { best = l } if r < data.count && isHigherPriority(data[r], data[best]) { best = r } if best == i { break } data.swapAt(i, best) i = best } } } // Min-heap var minH = Heap<Int>(<) // Max-heap var maxH = Heap<Int>(>)

Practice writing this until it takes three minutes. The interviewer will not give you partial credit for being surprised by its absence. For a deeper look at how the heap works under the hood, see this explainer.

Obama giving himself a medal: "AI reviewing AI-generated code", Swift engineer giving themselves a medal for writing a heap from scratch in a coding interview

Swift stdlib: no heap. Swift engineer mid-interview: builds one, gives self the medal.

One-Liners Worth Memorizing

compactMap is the function you reach for when mapping might produce nils. It maps and filters nil results in one pass, which is almost always what you want:

let strings = ["1", "two", "3", "four"] let nums = strings.compactMap { Int($0) } // [1, 3]

enumerated() gives you (offset, element) pairs. The offset is always zero-based, even on a slice. Don't assume. If you forget this and your interleaving logic mysteriously produces wrong results on slices, this is why.

zip is the clean way to walk two sequences in parallel without an index:

for (a, b) in zip(left, right) { }

stride replaces the C-style reverse loop that Swift deliberately does not allow:

for i in stride(from: n - 1, through: 0, by: -1) { } for i in stride(from: 0, to: n, by: 2) { } // even indices

reduce(into:) is more efficient than reduce for building collections because it mutates in place rather than copying at every step:

let freq = s.reduce(into: [:]) { acc, c in acc[c, default: 0] += 1 }

Five Gotchas That Compile Fine

These are the ones that pass the type checker, crash at runtime, and look completely correct on a first read. None of them produce a compiler warning. The compiler is fine with all of it. It is you and the judge who suffer.

1. Character.asciiValue is UInt8, not Int. Arithmetic with it silently truncates or overflows. Always cast to Int before doing math. Always.

2. ArraySlice indices are not zero-based. When you take array[2...5], the resulting slice has indices 2 through 5, not 0 through 3. Accessing slice[0] crashes. This comes up when passing slices to recursive functions, which comes up constantly.

let arr = [10, 20, 30, 40] let slice = arr[1...] // slice[0] crashes. Use slice[1], or Array(slice) to reset indices.

3. Integer overflow panics in debug builds. Swift traps on overflow by default, unlike C or Java. Int.max + 1 crashes immediately rather than silently wrapping. In problems where you might overflow (BFS distances, large sums, anything where you're multiplying coordinates), clamp explicitly or use the overflow operators &+, &-, &* for intentional wrapping. The overflow behavior is correct design. It is just surprising the first time it kills your solution in a contest judge.

4. sort() inside a closure that captures a struct requires var. This shows up when a nested helper or closure tries to sort a captured array. Pass the array as an inout parameter instead. You will hit this right when you think you're done.

5. Closures cannot mutate self inside a struct. Either switch to a class for the solution type, or restructure to avoid closures that need mutation. The compiler's error message here is clear, but the fix isn't always obvious in the moment.

Swift Interview Quick Reference

TaskSwift idiom
Frequency mapfreq[c, default: 0] += 1
Repeat-fill arrayArray(repeating: val, count: n)
Sort descendingarr.sorted(by: >)
Convert String to charsArray(s)
ASCII valueInt(c.asciiValue!)
Filter nils after mapcompactMap { }
Step iterationstride(from:to:by:)
Parallel iterationzip(a, b)
Set membershipset.contains(x)
Min/max with closurearr.min(by: { })
First matcharr.first(where: { })

Two Things to Drill Before the Interview

The string indexing restriction and the missing heap are where Swift candidates lose the most time. Both are solvable with thirty minutes of deliberate prep. Convert strings to character arrays at the start of every string problem until it is reflex. Write the heap struct from scratch twice. Time yourself on the second attempt.

After that, Swift's expressiveness genuinely works in your favor. The type system prevents entire categories of runtime bugs. The closures are clean. The standard library methods read like English. You can focus on the algorithm instead of fighting the language, which is the whole point.

The part you cannot practice alone is narrating all of it out loud while someone watches and takes notes. SpaceComplexity runs voice-based mock interviews with rubric-based feedback so you can find out whether your habits hold under pressure before the actual interview tells you.

Further Reading