BS
BleepingSwift
Published on
5 min read
Intermediate

> Swift Regex Builder: A Type-Safe DSL for Pattern Matching

Share:

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:

Swift
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:

Swift
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:

Swift
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:

Swift
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

Swift
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

Swift
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

Swift
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:

Swift
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:

Swift
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:

Swift
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:

View 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.

subscribe.sh

// Stay Updated

Get notified when I publish new tutorials on Swift, SwiftUI, and iOS development. No spam, unsubscribe anytime.

>

By subscribing, you agree to our Privacy Policy.