-
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
128: Practice Exercise: Building a Custom Result Builder (DSL)
I've seen a lot of developers try to build "configuration" systems by just passing around arrays of objects. It works, but it always feels like you're fighting the language. If you've used SwiftUI, you know how satisfying it is to just list your views inside a closure without commas, brackets, or explicit return statements. That's the magic of result builders. Today, we're going to build our own to create a Domain Specific Language (DSL) for a notification rule engine.
The Friction of Manual Array Construction
Imagine we're building a system where a user can define a set of rules for when a notification should trigger. In a naive implementation, you'd probably define a Rule struct and a RuleSet that holds an array of them. To build a set of rules, you'd do something like this:
struct Rule {
let description: String
let priority: Int
}
struct RuleSet {
let rules: [Rule]
}
// The "naive" way
let myRules = RuleSet(rules: [
Rule(description: "User is premium", priority: 1),
Rule(description: "Device is on battery", priority: 2),
Rule(description: "Time is after 9 PM", priority: 3)
])
Now, this isn't "wrong"βit's perfectly valid Swift. But as your rules get more complex, the syntax gets noisy. You're constantly staring at commas and square brackets. If you want to conditionally add a rule based on a flag, you have to break out of the array initialization and use append(), which completely destroys the declarative feel of the code. It turns a "definition" into a series of "instructions."
Using @resultBuilder to Clear the Noise
We can get rid of that boilerplate by creating a result builder. A result builder is essentially a piece of glue that tells the Swift compiler: "Whenever you see a closure marked with this attribute, don't treat it as a standard function; instead, collect all the expressions inside and pass them to these specific methods."
To do this, we define a type (usually named with a Builder suffix) and mark it with @resultBuilder. The core requirement is a buildBlock method that takes a variadic list of our target type and returns a combined result.
@resultBuilder
struct RuleBuilder {
static func buildBlock(_ components: Rule...) -> [Rule] {
return components
}
}
struct RuleSet {
let rules: [Rule]
// We use the builder here in the initializer
init(@RuleBuilder _ builder: () -> [Rule]) {
self.rules = builder()
}
}
// Now look at the difference:
let myRules = RuleSet {
Rule(description: "User is premium", priority: 1)
Rule(description: "Device is on battery", priority: 2)
Rule(description: "Time is after 9 PM", priority: 3)
}
I love this because it shifts the focus from the container (the array) to the content (the rules). It reads like a configuration file rather than a piece of imperative code.
Handling Logic and Optionals
The real power comes when you want your DSL to handle more than just a static list. If you want to support if statements or for loops inside your RuleSet, you can't just use buildBlock. You have to implement buildOptional and buildEither.
When the compiler sees an if statement without an else, it calls buildOptional. If it sees an if/else, it calls buildEither. Without these, your builder is essentially a static list. By adding them, you allow the person using your DSL to inject actual logic into the declaration process without leaving the clean, declarative block.
The trade-off here is a bit of upfront complexity. You're writing more code in the builder to save the user of the builder from writing tedious code. In my experience, if the DSL is going to be used in more than three places in your codebase, the investment in the result builder pays for itself almost immediately in terms of readability and reduced merge conflicts.
π Practical Task
Exercise: Building a Mock API Response DSL
You are tasked with creating a DSL that allows developers to quickly mock API responses for testing. Instead of manually creating arrays of "Response Fields," you will build a ResponseBuilder.
Requirements:
- Create a
Fieldstruct with two properties:key: Stringandvalue: Any. - Create a
@resultBuilder` struct namedResponseBuilderthat can collect multipleFieldobjects into an array[Field]. - Create a
MockResponsestruct that takes aResponseBuilderclosure in its initializer. - Implement
buildOptionalwithin yourResponseBuilderso that a field can be conditionally added using anifstatement.
Test your implementation with the following scenario:
let isAdmin = true
let response = MockResponse {
Field(key: "status", value: 200)
Field(key: "version", value: "1.0")
if isAdmin {
Field(key: "debug_info", value: "Admin Access Granted")
}
}
Ensure that the final MockResponse contains three fields if isAdmin is true, and two fields if it is false.
There are no comments for now.