Swift
Completed
-
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
222: Solving Tree and Graph Problems in Swift
Imagine you're looking for a specific file on your hard drive. You start at the root directory, open a folder called "Projects," then "SwiftCourse," then "Lessons," and finally find "Lesson222.md." You've just performed a tree traversal. Now, imagine you're using a map app to find the fastest way from your house to a coffee shop. You might check a few different street combinations, some of which loop back on themselves or intersect at multiple points. That's a graph problem.
When we map these to code, the "folders" or "intersections" become Nodes (or Vertices), and the "contains" relationship or the "road" connecting them becomes the Edge. The big difference is that a tree is just a restricted version of a graph—it's a graph where there's only one path between any two nodes and no cycles. If you can go in a circle, you're officially dealing with a graph.
Why Classes Over Structs for Nodes
In previous lessons, I've pushed you toward structs for almost everything. But when we build trees and graphs, we usually switch to class. Why? Because nodes need to refer to other nodes. If you try to put a struct inside itself, Swift will scream at you because the size of the value would be infinite. While you could use an indirect enum, classes are the industry standard here because they provide reference semantics. When you update a node's value in a graph, you want every other node pointing to it to see that change immediately.
class Node<T> {
var value: T
var children: [Node<T>] = []
init(_ value: T) {
self.value = value
}
}
Going Deep with Depth-First Search
Depth-First Search (DFS) is like that one person who enters a maze and refuses to turn back until they hit a dead end. In Swift, the cleanest way to implement this is through recursion. You visit a node, then immediately dive into its first child, then that child's first child, and so on.
I often use DFS when I need to explore every single possibility or when I'm searching for something that is likely buried deep in the hierarchy. The call stack handles the "remembering" of where to go back to once you hit a leaf node.
func depthFirstSearch(node: Node<String>, target: String) -> Bool {
if node.value == target { return true }
for child in node.children {
if depthFirstSearch(node: child, target: target) {
return true
}
}
return false
}
Exploring the Neighborhood with Breadth-First Search
BFS is different. It's like a ripple in a pond. You check the root, then all of its immediate children, then all of their children. If you're looking for the shortest path in an unweighted graph, BFS is your only real option. Because it explores layer by layer, the first time you find your target, you can be certain you found the quickest way there.
Since we can't use recursion to go "wide," we use a Queue. I'll be honest: Swift doesn't have a built-in Queue type in the standard library, so we usually just use an Array and removeFirst(), even though that's technically O(n). For a production app, you'd want a proper doubly-linked list queue, but for most algorithm problems, an array suffices.
func breadthFirstSearch(root: Node<String>, target: String) -> Bool {
var queue: [Node<String>>] = [root]
while !queue.isEmpty {
let current = queue.removeFirst()
if current.value == target { return true }
queue.append(contentsOf: current.children)
}
return false
}
Handling the "Infinite Loop" in Graphs
Here is where graphs get dangerous. In a tree, you can't get lost because there are no loops. In a graph, you can have a node that points back to its own ancestor. If you run the DFS or BFS code above on a graph with a cycle, your app will crash with a stack overflow or run forever.
The fix is simple but mandatory: keep a Set of visited nodes. Before you process a node, check if it's in the set. If it is, skip it. If not, add it and keep moving. Since we are using classes, we can make our Node conform to Hashable by using the object's identity (ObjectIdentifier) to ensure we're tracking the specific instance, not just a node with the same value.
📋 Practical Task
Build a Package Dependency Resolver
In software engineering, a "Dependency Graph" represents which libraries your project needs. Some libraries depend on other libraries, and sometimes you have circular dependencies (which is usually a bad thing!).
Your Task: Create a PackageNode class and a DependencyResolver class. The resolver should have a method called detectCircularDependency(root: PackageNode) -> Bool.
Requirements:
PackageNodeshould have aname: Stringand a list ofdependencies: [PackageNode].- The resolver must use a Depth-First Search (DFS) approach.
- You must track the "current path" of the recursion. If you encounter a node that is already in the current recursion stack, you've found a cycle.
- Test your code with two scenarios:
- A healthy chain: App $\rightarrow$ NetworkLib $\rightarrow$ LoggingLib (Should return
false). - A circular chain: App $\rightarrow$ NetworkLib $\rightarrow$ LoggingLib $\rightarrow$ App (Should return
true).
- A healthy chain: App $\rightarrow$ NetworkLib $\rightarrow$ LoggingLib (Should return
There are no comments for now.