-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Optionals
-
Section 4: Object-Oriented and Value Types
-
Section 5: Memory Management
-
Section 6: Generics and Error Handling
-
Section 7: Concurrency
-
Section 8: Working with Collections
-
Section 9: Codable and Data Handling
-
Section 10: Protocol-Oriented Programming
-
Section 11: Testing and Tooling
-
Section 12: Practical Projects
-
Section 13: Interview Practice
-
Section 14: More Practice Exercises
-
Section 15: More Standard Library
-
Section 16: Advanced Concurrency
-
Section 17: Foundation Framework Deep Dive
-
Section 18: URLSession and Networking Deep Dive
-
Section 19: Combine Framework
-
Section 20: SwiftUI Fundamentals for Swift Developers
-
Section 21: Server-Side Swift with Vapor
-
Section 22: Swift Package Manager Deep Dive
-
Section 23: Swift Concurrency Deep Dive
-
Section 24: More Language Features
-
Section 25: Error Handling Deep Dive
-
Section 26: Testing Deep Dive
-
Section 27: Data Structures and Algorithms in Swift
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Swift Macros (Swift 5.9+)
-
Section 31: Property Wrappers Ecosystem
-
Section 32: Swift Interop Deep Dive
-
Section 33: iOS App Architecture Patterns
-
Section 34: Performance and Debugging
-
Section 35: App Distribution and CI/CD
-
Section 36: More Practical Projects
-
Section 37: SwiftData and Persistence
-
Section 38: More Design Patterns
-
Section 39: More Review and Practice
-
Section 40: More Foundation Deep Dive
-
Section 41: Advanced Collections in Swift
-
Section 42: Advanced Generics Practice
-
Section 43: UIKit for Legacy and Hybrid Apps
-
Section 44: watchOS and visionOS Development Basics
-
Section 45: More Networking Patterns
-
Section 46: More Testing Practice
-
Section 47: Accessibility in Swift Apps
-
Section 48: Localization
-
Section 49: More Practical Projects Round 2
-
Section 50: Swift Charts Framework
-
Section 51: More Interview and Algorithm Practice
-
Section 52: Final Practice and Mastery
-
Section 53: Swift Compiler and Build System
-
Section 54: More Concurrency Practice
-
Section 55: App Store Guidelines and Review
-
Section 56: More Design and Architecture
132: Whiteboard Practice: Implementing a Linked List in Swift
A few years ago, I was interviewing a candidate for a senior role. They were brilliant with SwiftUI and knew the Combine framework better than I did, but when I asked them to implement a basic singly linked list on the whiteboard, they completely froze. They kept trying to use an Array to simulate the behavior, which defeated the entire purpose of the exercise. It was a classic case of "framework fluency" masking a gap in fundamental data structure knowledge. In a real production environment, you'll mostly use Swift's built-in collections, but understanding linked lists is how you stop thinking about data as a contiguous block of memory and start thinking about it as a web of references. This shift is critical when you start dealing with complex graphs or custom memory management.
Designing the Node and the List
In Swift, you can't build a linked list using structs because a node needs to hold a reference to another node of the same type. If you tried this with a struct, the compiler would scream at you about "value type having infinite size." We need classes here because they are reference types, allowing us to create that recursive chain.
First, we define the Node. I always recommend making this a generic class so your list can hold integers, strings, or custom objects without rewriting the logic. The most important part is the next property, which must be an optional. Why? Because the very last node in your list points to nothing—it's the end of the line.
class Node<T> {
var value: T
var next: Node<T>?
init(value: T) {
self.value = value
}
}
Now, we wrap that node in a LinkedList class. This class doesn't actually hold the data itself; it just keeps track of the head—the first node in the chain. If the head is nil, your list is empty. It's a simple entry point, but it's the only way you can navigate the rest of the structure.
class LinkedList<T> {
var head: Node<T>?
func prepend(_ value: T) {
let newNode = Node(value: value)
newNode.next = head
head = newNode
}
}
Managing the Pointers
The real "whiteboard" challenge comes when you have to append an item to the end of the list. Unlike an array, where you just call append() and the language handles the memory, a linked list requires you to manually "walk" from the head all the way to the tail. I've seen developers forget to handle the empty-list case here, which leads to a crash when they try to access a property on a nil head.
To do this correctly, you create a temporary pointer—usually called current—and use a while loop to move forward until current.next is nil. Once you've reached that final node, you simply attach your new node to it. It's a bit tedious, but it illustrates why linked lists have $O(n)$ time complexity for appending (unless you maintain a separate tail pointer, which is a great optimization to mention in an interview).
func append(_ value: T) {
let newNode = Node(value: value)
guard let headNode = head else {
head = newNode
return
}
var current = headNode
while let nextNode = current.next {
current = nextNode
}
current.next = newNode
}
When you're practicing this, pay close attention to how you're handling the optionals. Swift's while let and guard let patterns make this much safer than doing it in C++, but the logic remains the same: you are manually stitching memory addresses together. If you lose the reference to the head, you've effectively lost the entire list to the garbage collector (or ARC in Swift's case).
📋 Practical Task
Implementing a Custom Undo Manager via Linked List
In this exercise, you will build a simplified "Undo Manager" that stores a history of strings representing user actions. Because we only care about the most recent action (the head), a linked list is an efficient choice for this pattern.
Requirements:
- Create a
Nodeclass and aUndoManagerclass using the linked list logic from the lesson. - Implement a method
recordAction(_ action: String)that prepends a new action to the list. - Implement a method
undo() -> String?that removes the current head of the list and returns the action that was just "undone." - Implement a method
showHistory()that prints all recorded actions from newest to oldest.
Test your implementation with this scenario:
- Record "Typed 'Hello'"
- Record "Changed font to Bold"
- Record "Inserted Image"
- Call
showHistory()(Should see: Inserted Image → Changed font to Bold → Typed 'Hello') - Call
undo()(Should return "Inserted Image" and remove it from the list) - Call
showHistory()(Should see: Changed font to Bold → Typed 'Hello')
There are no comments for now.