12.10 — Live Coding Playbook

Opening scenario

Screen share opens. Interviewer says: “Implement an LRU cache in Swift, generic over key and value.” You have 30 minutes. The cursor blinks. You feel your heart rate spike. What you do in the next 60 seconds matters more than the code you eventually write.

The narration rule

Talk continuously. Silent typing is hostile to the interviewer — they can’t tell if you’re stuck or thinking. Narrate at three altitudes:

  • What you’re about to do: “I’ll start with the signature.”
  • Why you’re choosing it: “Generic over Key and Value; Key needs Hashable.”
  • What you’d revisit: “I’ll come back to thread safety after correctness.”

Even silent thinking takes voice: “Let me think for ten seconds about the eviction order.” That signals scope and gives the interviewer permission to wait.

The clarification ritual

Before typing, ask 3–5 questions. This is expected — not asking is the red flag.

For LRU cache:

  1. “Fixed capacity at init, or resizable?”
  2. “Thread safety required?”
  3. “Should get count as a use (move to front)?”
  4. “Are we optimizing for read-heavy or balanced workload?”
  5. “Should set of existing key update the value or just bump recency?”

Write the answers in a comment. They become your spec.

// Spec:
// - Fixed capacity at init
// - Single-threaded (caller's problem)
// - get bumps recency
// - set of existing key updates value AND bumps recency
// - Evict least-recently-used on overflow

The common iOS live-coding patterns

You should be able to write each of these blind in 10 minutes.

LRU cache

final class LRUCache<Key: Hashable, Value> {
    private let capacity: Int
    private var dict: [Key: Node] = [:]
    private var head: Node?       // most recently used
    private var tail: Node?       // least recently used

    private final class Node {
        let key: Key
        var value: Value
        var prev: Node?
        var next: Node?
        init(_ k: Key, _ v: Value) { key = k; value = v }
    }

    init(capacity: Int) {
        precondition(capacity > 0)
        self.capacity = capacity
    }

    func get(_ key: Key) -> Value? {
        guard let node = dict[key] else { return nil }
        moveToFront(node)
        return node.value
    }

    func set(_ key: Key, _ value: Value) {
        if let node = dict[key] {
            node.value = value
            moveToFront(node)
            return
        }
        let node = Node(key, value)
        dict[key] = node
        addToFront(node)
        if dict.count > capacity, let lru = tail {
            removeNode(lru)
            dict.removeValue(forKey: lru.key)
        }
    }

    private func addToFront(_ node: Node) {
        node.next = head
        head?.prev = node
        head = node
        if tail == nil { tail = node }
    }

    private func removeNode(_ node: Node) {
        node.prev?.next = node.next
        node.next?.prev = node.prev
        if head === node { head = node.next }
        if tail === node { tail = node.prev }
        node.prev = nil; node.next = nil
    }

    private func moveToFront(_ node: Node) {
        guard head !== node else { return }
        removeNode(node)
        addToFront(node)
    }
}

Narration cues: “I’m using a doubly-linked list + dictionary for O(1) get and set; dictionary maps key to node, list maintains LRU order.”

Debounce

actor Debouncer {
    private let delay: Duration
    private var task: Task<Void, Never>?

    init(delay: Duration) { self.delay = delay }

    func call(_ action: @escaping @Sendable () async -> Void) {
        task?.cancel()
        task = Task {
            try? await Task.sleep(for: delay)
            guard !Task.isCancelled else { return }
            await action()
        }
    }
}

// Usage:
let d = Debouncer(delay: .milliseconds(300))
await d.call { await search(query: text) }

Thread-safe counter (actor)

actor Counter {
    private(set) var value = 0
    func increment() { value += 1 }
    func decrement() { value -= 1 }
}

If asked for a pre-actor version: DispatchQueue with .barrier flag for writes.

Simple @Observable from scratch

@propertyWrapper
struct Tracked<Value> {
    private var storage: Value
    var wrappedValue: Value {
        get { storage }
        set { storage = newValue; notify() }
    }
    init(wrappedValue: Value) { storage = wrappedValue }
    var listeners: [(Value) -> Void] = []
    mutating func subscribe(_ cb: @escaping (Value) -> Void) {
        listeners.append(cb)
    }
    private func notify() { listeners.forEach { $0(storage) } }
}

Useful when interviewer asks “how does @Observable work under the hood?”

Async image fetcher with cancellation

actor ImageLoader {
    private var cache: [URL: UIImage] = [:]
    private var inFlight: [URL: Task<UIImage, Error>] = [:]

    func image(for url: URL) async throws -> UIImage {
        if let cached = cache[url] { return cached }
        if let task = inFlight[url] { return try await task.value }
        let task = Task<UIImage, Error> {
            let (data, _) = try await URLSession.shared.data(from: url)
            guard let img = UIImage(data: data) else { throw URLError(.cannotDecodeContentData) }
            return img
        }
        inFlight[url] = task
        defer { inFlight[url] = nil }
        let img = try await task.value
        cache[url] = img
        return img
    }
}

SwiftUI live coding expectations

For SwiftUI questions (“build a TODO app live”):

  • Start with the model (struct Todo: Identifiable, Hashable).
  • Then @Observable store (TodoStore).
  • Then root view with @State store, NavigationStack.
  • Then List + ForEach with add/delete.
  • Comment on @Observable vs @StateObject if iOS 17 is allowed.
  • Talk about persistence: “If we wanted to persist, I’d reach for SwiftData.”

The 3-step stuck recovery

You’re 15 minutes in. Stuck. What to do:

  1. Verbalize the gap: “I’m stuck because I’m not sure how to handle the case where X.” Naming the obstacle often reveals the fix.
  2. Reduce the problem: “Let me solve the simpler version first — without thread safety / generics / cancellation — and then add the missing piece.”
  3. Ask for a small hint: “Could I get a nudge on the data structure?” — Interviewers expect this; they’re rooting for you.

Never silently flounder. Silence past 30 seconds is the red flag.

What interviewers score

  • Speed to first working code: 10–15 min for a working naive solution beats 30 min of perfect code that doesn’t run.
  • Test mentality: even a quick let cache = LRUCache<String, Int>(capacity: 2); cache.set("a",1); cache.set("b",2); cache.set("c",3); assert(cache.get("a") == nil) shows discipline.
  • Tradeoff verbalization: “If write-heavy, I’d switch to…”
  • Composure under correction: when the interviewer says “what if capacity is 0?” your reaction tells them everything.

Common misconceptions

  1. “Live coding tests algorithms.” Mostly it tests communication under uncertainty. The algorithm is the medium.
  2. “You should code in silence to focus.” Silence loses you points even with perfect code.
  3. “Optimize prematurely.” Get it working first. Optimization talk comes after.
  4. “Tests are skippable.” A 30-second sanity test impresses more than another minor optimization.
  5. “Compile errors lose points.” Honest typos are fine; live-fix and move on. Conceptual errors are what matter.

Seasoned engineer’s take

Live coding is performance art with code. The interviewer is hiring a teammate — they’re judging “do I want to pair with this person on a hard problem?” The code matters; the collaboration vibe matters more. Be the person who narrates clearly, asks good questions, recovers gracefully, and tests their work. The exact algorithm choice rarely decides the outcome.

TIP: Practice in a code editor without autocompletion. Your future interview will be in a shared web editor with mediocre tooling. Build the muscle of writing Swift from memory.

WARNING: Do not use AI assistance during live interviews unless explicitly invited. Even if not banned, it reads as poor judgment.

Interview corner

Junior: “What’s the first thing you do when given a live coding problem?” Restate the problem and ask 2–3 clarifying questions to lock down the spec. Then write the type signature.

Mid: “How do you handle being stuck mid-interview?” Verbalize the obstacle, reduce to a simpler problem, then either solve the simpler version or ask for a small hint. Silence is what hurts.

Senior: “How do you balance speed vs correctness in live coding?” I aim for a working naive solution first — even O(n²) is fine for round one — then iterate to optimal once tests pass. I narrate the tradeoff: ‘this is O(n²) but readable; if perf matters I’d switch to a heap.’ The interviewer learns that I think in tradeoffs and can ship a correct-but-imperfect solution under pressure, which mirrors actual production work.

Red-flag answer: “I just code it in silence; talking slows me down.” Senior interviews look for collaboration, not code-golf speed.

Lab preview

Lab 12.3 (mock technical interview) includes a live-coding section with 5 problems, timer, and rubric. Pair with a friend and switch roles each week.


Next: 12.11 — Behavioral STAR Templates