Swift Coding Interview Gotchas: The Footguns Nobody Warns You About

- Swift strings can't be indexed with integers — convert to
Array(s)once at the top of your function for O(1) random access removeFirst()in a BFS loop is O(n) per call and O(n²) total; use a head index pointer instead- Dictionary subscript returns
T?, notT; usedict[key, default: 0] += 1to avoid compile errors on frequency counting - Swift has no built-in heap — memorize a MinHeap implementation before your interview or know which problems can avoid one
- Arrays and dicts are value types (structs); mutations inside functions vanish unless you use
inoutand call with& - Integer overflow crashes Swift at runtime; use
Int.max / 2as your Dijkstra infinity sentinel, notInt.max - Sort comparators require strict weak ordering — always use
<, never<=, or risk undefined behavior
Swift is genuinely pleasant to write. The type system catches mistakes early, the standard library is expressive, and the language forces you to think about optionality. In a 45-minute interview, a few of those same design decisions will quietly wreck you if you haven't seen them before.
These aren't obscure edge cases. They're the things that cost candidates real time: a BFS that becomes O(n²), a mutation that silently disappears, a Dijkstra crash on the very first relaxation step. Burn through them once before your interview and you won't hit them on the clock.
You Cannot Index a Swift String With an Integer
This is the biggest Swift gotcha, full stop. It catches engineers from every other language and it will catch you if you walk in unprepared.
let s = "hello" let c = s[2] // COMPILE ERROR
Swift strings use String.Index, not integers, because Swift strings are Unicode-correct. A "character" can span multiple bytes. The compiler refuses to let you pretend otherwise, and it does this at you immediately, loudly, in an interview.
The fix most candidates reach for first is wrong:
// Wrong: O(n) on every access, and you still need an Index let idx = s.index(s.startIndex, offsetBy: 2) let c = s[idx] // works, but building idx costs O(n)
Every other language in your prep let you do s[2]. Swift says no. The correct move is to convert once and work on an array:
let chars = Array(s) // O(n) once let c = chars[2] // O(1) every time
For most interview problems involving character-by-character work, Array(s) at the top of your function is the right call. Do it once, work on the array, convert back with String(chars) if you need to return a string.
The binary search on strings trap is related. If you try to binary search on a string's characters without converting first, you'll write O(n) index construction inside a loop and end up with O(n log n) for what should be O(log n). More on binary search invariants in this guide.
removeFirst() Is O(n). Your BFS Just Became O(n²) and Nobody Told You.
You've written a clean BFS. The logic is correct, the structure is elegant, and the algorithm is secretly quadratic. Congratulations.
Arrays in Swift are contiguous in memory. Removing the first element shifts every remaining element left. That's O(n) per dequeue. In a BFS where you're doing this on every iteration, you've just turned a linear algorithm into a quadratic one:
// Do not do this for BFS var queue = [startNode] while !queue.isEmpty { let node = queue.removeFirst() // O(n) each time -- total: O(V²) // ... }
The fix requires no extra data structure. A head index is enough:
var queue = [startNode] var head = 0 while head < queue.count { let node = queue[head] head += 1 // process, append neighbors to queue }
This pattern is O(1) per dequeue and O(V+E) total. The tradeoff is memory: the array grows and never shrinks. For interview-scale inputs, that's fine. For a graph with 100,000 nodes, a silently quadratic BFS is not.
If you need a proper double-ended queue, the Swift Collections package provides Deque<Element> with O(1) operations at both ends. Most online judges only have the stdlib, so the head-index trick is your portable fallback.
Dictionary Subscript Returns Optional (and In-Place Mutation Has a Catch)
dict[key] returns T?, not T. This is by design, since the key might not exist. It creates two separate traps.
Trap 1: You try to do math on the result and get a type error.
var freq = [Character: Int]() for c in s { freq[c] += 1 // COMPILE ERROR: value of optional type 'Int?' not unwrapped }
The fix is the default-value subscript:
freq[c, default: 0] += 1 // works correctly
Trap 2, subtler: The force-unwrap version works until it doesn't.
freq[c]! += 1 // crashes the moment c isn't in the dict yet
This works exactly once, on the first character that already has a count. The second time you see a fresh key, it crashes. During an interview, this is a gift to no one.
Always use dict[key, default: value] for frequency counting and accumulation. It handles first insertion cleanly, no if let dance required.
There Is No Built-In Heap
Swift's standard library has no heap type. This is the stdlib equivalent of reaching for a spatula and finding the drawer completely empty. The problem exists. The tool does not.
The naive workaround candidates reach for is sorting after each insertion:
var minHeap = [Int]() minHeap.append(val) minHeap.sort() // O(n log n) per insertion
That turns a problem solvable in O(n log n) total into O(n² log n). On inputs of size 10,000, the difference is roughly "passes in 100ms" versus "times out while your interviewer watches their clock."
Implement a heap before your interview and commit it to memory, or know which problems you can dodge. For top-K problems you can sometimes sort the whole array at the end. For streaming median or k-closest, you genuinely need a heap. Know which is which before you sit down.
The heap invariants and sift operations are covered in the heap data structure guide. Here's a minimal Swift implementation worth having ready:
struct MinHeap { var data = [Int]() mutating func push(_ val: Int) { data.append(val) siftUp(data.count - 1) } mutating func pop() -> Int? { guard !data.isEmpty else { return nil } data.swapAt(0, data.count - 1) let val = data.removeLast() if !data.isEmpty { siftDown(0) } return val } var top: Int? { data.first } var isEmpty: Bool { data.isEmpty } private mutating func siftUp(_ i: Int) { var i = i while i > 0 { let parent = (i - 1) / 2 if data[parent] > data[i] { data.swapAt(i, parent) } else { break } i = parent } } private mutating func siftDown(_ i: Int) { var i = i while true { var smallest = i let l = 2*i+1, r = 2*i+2 if l < data.count && data[l] < data[smallest] { smallest = l } if r < data.count && data[r] < data[smallest] { smallest = r } if smallest == i { break } data.swapAt(i, smallest) i = smallest } } }
Value Semantics Will Eat Your Mutations
Arrays, dictionaries, and sets in Swift are all structs. Value types. When you pass them to a function or capture them in a closure, you get a copy. Your changes stay in that copy. The original never hears about them.
func addElement(_ arr: [Int]) { var arr = arr arr.append(99) // mutates the local copy } var myArray = [1, 2, 3] addElement(myArray) print(myArray) // [1, 2, 3] -- 99 is gone, somewhere in a stack frame that no longer exists
This bites hardest in graph traversal where you pass a visited set into a recursive DFS:
func dfs(_ node: Int, _ visited: Set<Int>) { var visited = visited // fresh copy per call frame visited.insert(node) // doesn't affect the caller's visited set // ... }
Every recursive frame gets its own copy of visited, so you'll revisit nodes forever. On a graph with a cycle, this means infinite recursion. On a graph without one, it means exponential work. Neither is what you wanted.
The fix is inout:
func dfs(_ node: Int, _ visited: inout Set<Int>) { visited.insert(node) // changes propagate to the caller } // call site: dfs(start, &visited)
This is also why backtracking in Swift requires explicit undo steps. You're not relying on copy-on-return to restore state. You have to restore it yourself, which is fine once you've internalized that this is how the language works.
Integer Overflow Crashes. It Does Not Wrap Silently.
In C or Go, Int.max + 1 wraps around to a negative number and causes mysterious, hard-to-reproduce bugs. Swift takes a more direct approach: it crashes immediately with a fatal error. Brutal, but at least you know exactly where the problem is.
let big = Int.max let bigger = big + 1 // Fatal error: integer overflow
This comes up in Dijkstra when you initialize distances to "infinity":
var dist = Array(repeating: Int.max, count: n) // Later: if dist[u] + weight < dist[v] { // crash if dist[u] is Int.max
Use Int.max / 2 as your infinity value, not Int.max. Adding any real edge weight to Int.max / 2 won't overflow, and it's still absurdly large for any interview input.
If you actually need wrapping arithmetic, use the overflow operators:
let wrapped = Int.max &+ 1 // no crash, wraps to Int.min
Set.insert() Returns a Tuple, Not a Bool
This one triggers a compile error so you'll catch it before runtime. It still costs you time while someone is watching:
var seen = Set<Int>() if seen.insert(5) { // COMPILE ERROR // ... }
Set.insert(_:) returns (inserted: Bool, memberAfterInsert: Element). If you want to check whether the element was new:
if seen.insert(5).inserted { // only runs if 5 wasn't already in the set }
Most of the time in interview code you don't need the return value at all. seen.insert(5) by itself is fine and the compiler won't complain about a discarded tuple.
Sort Comparators Must Return False on Equal Elements
Swift's sort(by:) requires a strict weak ordering. That means it must return false when the two elements are equal. Using <= instead of < is technically undefined behavior, which in practice means wrong results or a crash:
// WRONG: using <= instead of < arr.sort { $0 <= $1 } // undefined behavior // CORRECT arr.sort { $0 < $1 }
If you're sorting by multiple keys, handle equal cases explicitly:
meetings.sort { if $0.start != $1.start { return $0.start < $1.start } return $0.end < $1.end }
Using <= at the top level there breaks the secondary sort entirely. Two meetings with the same start time return true immediately, before the second key is ever checked. This is the kind of bug that's obvious in hindsight and completely invisible when you're 35 minutes in with someone watching.
Swift Coding Interview Gotchas: Quick Reference
| Footgun | What Happens | Fix |
|---|---|---|
s[2] on a String | Compile error | Array(s)[2] |
queue.removeFirst() in BFS | O(n) per call, O(n²) total | Head index pointer |
dict[key] += 1 | Compile error (returns Optional) | dict[key, default: 0] += 1 |
| No PriorityQueue | Forced to sort on each insert | Implement MinHeap and memorize it |
Passing [Int] to func | Mutation invisible to caller | inout parameter, & at call site |
dist = Array(repeating: Int.max, ...) | Overflow crash on first relaxation | Int.max / 2 as infinity |
if seen.insert(x) | Compile error (tuple, not Bool) | if seen.insert(x).inserted |
sort { $0 <= $1 } | Wrong results or crash | sort { $0 < $1 } |
One Thing LeetCode Practice Won't Teach You
You can internalize every one of these gotchas and still struggle in an interview. The reason is that running code on LeetCode gives you a compiler and a judge. An interview gives you a blank screen, someone watching, and a timer. The muscle memory you need is different.
The only thing that builds that muscle is practicing in that environment. If you're prepping in Swift, try SpaceComplexity for voice-based mock interviews with real-time feedback on both your code and your narration. Catching a value semantics bug while talking through your approach is a genuinely different skill from catching it after a failed submit.
For the full Swift standard library reference and collection decision table, see Swift for Coding Interviews. To see how Swift compares to Kotlin on library gaps, check Swift vs Kotlin for Coding Interviews.
Further Reading
- The Swift Programming Language (docs.swift.org). The authoritative language reference.
- Swift Standard Library (developer.apple.com). Full API docs for all collections.
- apple/swift-collections on GitHub. Deque, OrderedSet, and other missing stdlib types.
- Dictionary subscript(_:default:) (Apple Developer Docs). Official docs on the default-value subscript.
- Array.removeFirst() complexity (Swift Forums). The thread confirming O(n) and discussing alternatives.