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
78: Scanner for Parsing Text
I've spent a lot of time using Regular Expressions for parsing text, and while they're powerful, they can quickly become an unreadable mess of backslashes and parentheses. When I need to walk through a string and pull out specific types of data—like a number, then a word, then another number—I reach for Scanner. It's part of Foundation and behaves like a cursor moving across your text, letting you pluck out values as you go.
Let's build something practical: a simple parser for a recipe ingredient list. Imagine we have a string like "2 cups flour, 1.5 tsp salt, 3 eggs" and we want to turn that into a list of structured objects.
Setting up the Scan
First, we need a simple struct to hold our data. I'll keep it basic: a quantity (Double) and a name (String). To use Scanner, we initialize it with the string we want to parse. I prefer using the charactersToBeSkipped property to tell the scanner to ignore whitespace automatically, which saves us from manually skipping spaces between the number and the ingredient name.
struct Ingredient {
let quantity: Double
let name: String
}
let recipeText = "2 cups flour, 1.5 tsp salt, 3 eggs"
let scanner = Scanner(string: recipeText)
scanner.charactersToBeSkipped = .whitespacesAndNewlines
Extracting the Quantities
The beauty of Scanner is the type-specific methods. Instead of slicing the string and trying to cast it to a Double, I can just call scanDouble(). If the scanner finds a number at its current position, it consumes it and returns it; otherwise, it returns nil.
var ingredients: [Ingredient] = []
while !scanner.isAtEnd {
if let quantity = scanner.scanDouble() {
// Now we need the name...
}
}
The "Stop Character" Trap
Here is where I usually trip up the first time I use this. I need to get the name of the ingredient, but the ingredients are separated by commas. I'll use scanUpTo, which reads everything until it hits a specific character. Let's see what happens when I try to grab the name.
// My first attempt (which had a bug)
if let quantity = scanner.scanDouble() {
if let name = scanner.scanUpTo(", ") {
ingredients.append(Ingredient(quantity: quantity, name: name))
}
}
I ran this and noticed something annoying: the first ingredient was "cups flour", but the second one was ", tsp salt". Why? Because scanUpTo stops at the character you specify, but it doesn't consume it. The comma is still sitting there at the front of the cursor, so the next time the loop runs, scanDouble() fails immediately because it sees a comma, not a number.
To fix this, I have to manually tell the scanner to skip that comma if it's there. I'll use scanString to move the cursor past the delimiter.
Finalizing the Parser Loop
Now that I've accounted for the trailing comma, the logic holds up. I'll wrap it in a loop that continues until the cursor reaches the end of the string. I'm adding a small check to make sure we don't get stuck in an infinite loop if the scanner encounters a character it doesn't expect.
var ingredients: [Ingredient] = []
let scanner = Scanner(string: recipeText)
scanner.charactersToBeSkipped = .whitespacesAndNewlines
while !scanner.isAtEnd {
if let quantity = scanner.scanDouble() {
// Scan everything until the comma
if let name = scanner.scanUpTo(",") {
ingredients.append(Ingredient(quantity: quantity, name: name))
}
// IMPORTANT: Consume the comma so we can move to the next item
_ = scanner.scanString(",")
} else {
// If we can't find a number, just move forward one char to avoid infinite loop
_ = scanner.scanCharacter()
}
}
for item in ingredients {
print("Found: \(item.quantity) of \(item.name)")
}
This approach is much cleaner than splitting the string into an array and then splitting those arrays again. You're treating the string as a stream, which is exactly how parsing should feel.
📋 Practical Task
Build a Game Map Coordinate Parser
In many old-school game save files, coordinates are stored as a single string of pairs, such as: "X:10 Y:20 X:15 Y:30 X:40 Y:10".
Your task is to create a Coordinate struct (with x and y as Ints) and use a Scanner to parse the string above into an array of Coordinate objects.
Requirements:
- Use
scanString("X:")andscanString("Y:")to navigate the labels. - Use
scanInt()to extract the numeric values. - Ensure the scanner correctly consumes all parts of the string so that you end up with an array of 3
Coordinateobjects.
There are no comments for now.