- Published on
- 5 min read Intermediate
> Swift Regex Builder: A Type-Safe DSL for Pattern Matching
Regular expressions are powerful but notoriously hard to read. Swift 5.7 introduced RegexBuilder, a domain-specific language that lets you build regex patterns using Swift syntax instead of cryptic character sequences.
The Problem with Traditional Regex
Consider parsing a date string like "2026-03-15". With a traditional regex literal, you'd write something like:
let dateRegex = /(\d{4})-(\d{2})-(\d{2})/
It works, but what does each capture group represent? You have to count parentheses and remember the order. Now imagine a more complex pattern with optional groups and alternatives.
Enter RegexBuilder
RegexBuilder lets you express the same pattern declaratively:
import RegexBuilder
let dateRegex = Regex {
Capture {
Repeat(.digit, count: 4)
}
"-"
Capture {
Repeat(.digit, count: 2)
}
"-"
Capture {
Repeat(.digit, count: 2)
}
}
This is more verbose, but the intent is clear. Each component is labeled, and the structure mirrors how you'd describe the pattern in words.
Strongly-Typed Captures
The real power of RegexBuilder is type-safe captures. You can transform captured values as part of the regex:
import RegexBuilder
let dateRegex = Regex {
TryCapture {
Repeat(.digit, count: 4)
} transform: { substring in
Int(substring)
}
"-"
TryCapture {
Repeat(.digit, count: 2)
} transform: { substring in
Int(substring)
}
"-"
TryCapture {
Repeat(.digit, count: 2)
} transform: { substring in
Int(substring)
}
}
let input = "2026-03-15"
if let match = input.wholeMatch(of: dateRegex) {
let year: Int = match.1 // 2026
let month: Int = match.2 // 3
let day: Int = match.3 // 15
print("Year: \(year), Month: \(month), Day: \(day)")
}
The captures are automatically typed as Int because of the transform closures. No string-to-int conversion needed at the call site.
Named References
For complex patterns, named captures make the code even clearer:
import RegexBuilder
let yearRef = Reference<Int>()
let monthRef = Reference<Int>()
let dayRef = Reference<Int>()
let dateRegex = Regex {
TryCapture(as: yearRef) {
Repeat(.digit, count: 4)
} transform: { Int($0) }
"-"
TryCapture(as: monthRef) {
Repeat(.digit, count: 2)
} transform: { Int($0) }
"-"
TryCapture(as: dayRef) {
Repeat(.digit, count: 2)
} transform: { Int($0) }
}
let input = "2026-03-15"
if let match = input.wholeMatch(of: dateRegex) {
print("Year: \(match[yearRef])")
print("Month: \(match[monthRef])")
print("Day: \(match[dayRef])")
}
Building Blocks
RegexBuilder provides components for common patterns.
Character Classes
import RegexBuilder
let regex = Regex {
One(.digit) // Single digit
OneOrMore(.word) // One or more word characters
ZeroOrMore(.whitespace)
CharacterClass(.digit, .anyOf(".-")) // Digit or . or -
}
Quantifiers
import RegexBuilder
let regex = Regex {
Repeat(.digit, count: 3) // Exactly 3
Repeat(.digit, 2...4) // 2 to 4
Repeat(.digit, 2...) // 2 or more
Repeat(.digit, ...4) // Up to 4
Optionally { "-" } // 0 or 1
ZeroOrMore { .word } // 0 or more
OneOrMore { .digit } // 1 or more
}
Alternation
import RegexBuilder
let protocolRegex = Regex {
ChoiceOf {
"http"
"https"
"ftp"
}
"://"
}
Real-World Example: Parsing Log Lines
Here's a practical example parsing a log line like [2026-03-15 14:32:05] ERROR: Connection failed:
import RegexBuilder
import Foundation
struct LogEntry {
let timestamp: Date
let level: String
let message: String
}
let timestampRef = Reference<Date>()
let levelRef = Reference<Substring>()
let messageRef = Reference<Substring>()
let logRegex = Regex {
"["
TryCapture(as: timestampRef) {
Repeat(.digit, count: 4)
"-"
Repeat(.digit, count: 2)
"-"
Repeat(.digit, count: 2)
" "
Repeat(.digit, count: 2)
":"
Repeat(.digit, count: 2)
":"
Repeat(.digit, count: 2)
} transform: { substring in
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
return formatter.date(from: String(substring))
}
"] "
Capture(as: levelRef) {
OneOrMore(.word)
}
": "
Capture(as: messageRef) {
OneOrMore(.any)
}
}
let line = "[2026-03-15 14:32:05] ERROR: Connection failed"
if let match = line.wholeMatch(of: logRegex) {
let entry = LogEntry(
timestamp: match[timestampRef],
level: String(match[levelRef]),
message: String(match[messageRef])
)
print("Level: \(entry.level), Message: \(entry.message)")
}
Mixing with Regex Literals
You can embed traditional regex literals inside RegexBuilder when you need a compact pattern:
import RegexBuilder
let emailRegex = Regex {
Capture {
OneOrMore {
CharacterClass(
.word,
.anyOf(".+-")
)
}
}
"@"
Capture {
/[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/
}
}
This gives you the best of both worlds: readable structure with concise inline patterns.
Performance Considerations
RegexBuilder patterns compile to the same underlying engine as regex literals, so performance is equivalent. The DSL is purely a compile-time abstraction.
For patterns you use repeatedly, store the compiled Regex rather than recreating it:
import RegexBuilder
// Good: compile once
let emailRegex = Regex {
OneOrMore(.word)
"@"
OneOrMore {
CharacterClass(.word, .anyOf(".-"))
}
}
func validateEmails(_ emails: [String]) -> [String] {
emails.filter { $0.wholeMatch(of: emailRegex) != nil }
}
When to Use RegexBuilder
RegexBuilder shines when you have complex patterns with multiple captures, when you need type transformations on captured values, or when readability matters more than brevity. For simple one-off patterns, regex literals are still fine.
The compiler catches errors at build time rather than runtime, which eliminates an entire class of bugs. If you've ever debugged a malformed regex in production, you'll appreciate this.
Sample Project
Want to see this code in action? Check out the complete sample project on GitHub:
The repository includes a working Xcode project with all the examples from this article, plus unit tests you can run to verify the behavior.
// Continue_Learning
The Difference Between Frame and Bounds in UIKit
Understand the key difference between a UIView's frame and bounds properties, and when to use each in your iOS apps.
TaskGroup and Structured Concurrency Patterns in Swift
Master Swift's TaskGroup for parallel async work with proper cancellation, error handling, and result collection. Learn common patterns for batch operations and concurrent pipelines.
Typed Throws in Swift 6: Declaring Specific Error Types
Swift 6 introduces typed throws, letting you declare exactly which error types a function can throw. Learn how this improves API contracts and enables exhaustive error handling.
// Stay Updated
Get notified when I publish new tutorials on Swift, SwiftUI, and iOS development. No spam, unsubscribe anytime.