Hey Swifters 🙂 You might have used regular expressions in the past and would already know how messy and hard to read they can get, well in this guide we will explore the RegexBuilder in Swift that makes our lives easier. Lets start with:
What Is RegexBuilder
?
RegexBuilder in Swift is a type-safe, compile checked way to construct regular expressions with Swift syntax instead of cryptic literals. You get autocompletion, strong typing, and instant compiler feedback on mistakes out of the box. Here is a quick taste of this new API.
import RegexBuilder
let digits = Regex {
OneOrMore(.digit)
}
let test = "099079"
print(test.firstMatch(of: digits) != nil)
// true
Not cryptic at all, right? Lets dig deep into every component, quantifier and capture.
Components
CharacterClass
Character classes let you define sets of characters to match. You can use predefined categories (like digits), custom ranges (e.g. a…z
), or specific listings (e.g. [aeiou]
). This is the building block for matching letters, numbers, or any custom group.
import RegexBuilder
// Lowercase letters only
// We want to match *only* strings made entirely of lowercase letters.
let lowercaseOnly = Regex {
Anchor.startOfSubject
OneOrMore("a"..."z")
Anchor.endOfSubject
}
print("hello matches?:", "hello".firstMatch(of: lowercaseOnly) != nil)
// true
print("Hello matches?:", "Hello".firstMatch(of: lowercaseOnly) != nil)
// false
print("hell0 matches?:", "hell0".firstMatch(of: lowercaseOnly) != nil)
// false
// Vowels only
let vowelsOnly = Regex {
CharacterClass.anyOf("aeiou")
}
print("First vowel:", "swift".firstMatch(of: vowelsOnly)?.0 ?? "none")
// "i"
// Not digits
let notDigits = Regex {
CharacterClass(.digit).inverted
}
print("First non-digit:", "123abc".firstMatch(of: notDigits)?.0 ?? "none")
// "a"
Anchor
A regex component that matches a specific condition at a particular position in an input string.
import RegexBuilder
do {
let startsWithHello = Regex {
Anchor.startOfSubject
"Hello"
}
print(try startsWithHello.firstMatch(in: "Hello, world!") != nil)
// true
print(try startsWithHello.firstMatch(in: "Well, Hello!") != nil)
// false
let endsWithPeriod = Regex {
"."
Anchor.endOfSubject
}
print(try endsWithPeriod.firstMatch(in: "Hi.") != nil)
// true
print(try endsWithPeriod.firstMatch(in: "Hi!") != nil)
// false
} catch {
print(error)
}
Lookahead
& NegativeLookahead
Lookahead
is a regex component that allows a match to continue only if its contents match at the given location. NegativeLookahead
is a regex component that allows a match to continue only if its contents do not match at the given location.
import RegexBuilder
do {
// Match “dog” only if immediately followed by “house”
let dogHouse = Regex {
"dog"
Lookahead { "house" }
}
print(try dogHouse.firstMatch(in: "doghouse") != nil)
// true
print(try dogHouse.firstMatch(in: "doggie") != nil)
// false
// Match “car” only if NOT followed by “pet”
let carNotPet = Regex {
"car"
NegativeLookahead { "pet" }
}
print(try carNotPet.firstMatch(in: "carpool") != nil)
// true
print(try carNotPet.firstMatch(in: "carpet") != nil)
// false
} catch {
print(error)
}
ChoiceOf
ChoiceOf
is like the “or” operator (|
) in traditional regex, it tries each alternative and picks the first one that matches.
import RegexBuilder
do {
let colorPicker = Regex {
ChoiceOf {
"red"
"green"
"blue"
}
}
try ["red", "yellow", "blue", "Green"].forEach {
print("\($0):", try colorPicker.firstMatch(in: $0) != nil)
}
// red:true, yellow:false, blue:true, Green:false
} catch {
print(error)
}
Quantifiers
One
Match exactly one occurrence, use One
when you explicitly want exactly one occurrence of a component. It’s the most literal way to say “exactly one.”
import RegexBuilder
do {
let singleHash = Regex { One("#") }
print(try singleHash.firstMatch(in: "er#fs#") != nil)
// true - Looks for at least one # in the string
print(try singleHash.wholeMatch(in: "##") != nil)
// false - Looks for exactly one # and nothing else in the string
} catch {
print(error)
}
Optionally
This
makes the preceding element optional, zero or one occurrence.
import RegexBuilder
do {
let signedNumber = Regex {
Optionally { ChoiceOf { "+"; "-" } }
OneOrMore(.digit)
}
try ["+42", "42", "-"].forEach {
print("\($0):", try signedNumber.firstMatch(in: $0) != nil)
}
// "+42" : true, "42" : true, "-" : false
} catch {
print(error)
}
"+42"
is true
beacuse it has the optional + and has two digits so true. “42” has no sign which if fine and has two digits so true again. Now “-” is the sign but it does not have any digits thats why false.
ZeroOrMore
This matches as many as possible, even zero of the given component.
import RegexBuilder
do {
let manyXsThenY = Regex {
ZeroOrMore("x")
"y"
}
try ["p", "iy", "xxxxxy"].forEach {
print("\($0):", try manyXsThenY.firstMatch(in: $0) != nil)
}
// "p" : false, "iy" : true, "xxxxxy" : true
} catch {
print(error)
}
OneOrMore
This requires at least one but potentially many of the component.
import RegexBuilder
do {
let repeatedAThenB = Regex {
OneOrMore("a")
"b"
}
try ["ab", "aaab", "b"].forEach {
print("\($0):", try repeatedAThenB.firstMatch(in: $0) != nil)
}
// "ab" : true, "aaab" : true, "b" : false
} catch {
print(error)
}
Repeat
A regex component that matches a selectable number of occurrences of its underlying component.
import RegexBuilder
do {
let exactlyThreeAs = Regex {
Repeat(count: 3) { "a" }
"b"
}
print(try exactlyThreeAs.firstMatch(in: "aaab") != nil)
// true
print(try exactlyThreeAs.firstMatch(in: "aab" ) != nil)
// false
let twoToFourBsThenC = Regex {
Repeat(2...4) { "b" }
"c"
}
try ["bbc", "bbbbc", "bc"].forEach {
print("\($0):", try twoToFourBsThenC.firstMatch(in: $0) != nil)
}
// "bbc" : true, "bbbbc" : true, "bc" : false
} catch {
print(error)
}
Local
This creates an atomic group, preventing the engine from backtracking inside it. Use this when you need to lock in a match early.
import RegexBuilder
do {
let atomicExample = Regex {
Local {
OneOrMore("a")
"b"
}
"b"
}
// Try "aab": the inner group must match "aab" then literal "b" fails, and no backtracking occurs.
print(try atomicExample.firstMatch(in: "aabb") != nil)
// true
print(try atomicExample.firstMatch(in: "aab") != nil)
// false
} catch {
print(error)
}
Captures
Capture
It saves whatever is matched inside it so that you can retrieve it later from the match object.
import RegexBuilder
do {
let dateCapture = Regex {
Capture { Repeat(count: 2) { .digit } } // Month
"/"
Capture { Repeat(count: 2) { .digit } } // Day
"/"
Capture { Repeat(count: 4) { .digit } } // Year
}
if let match = try dateCapture.firstMatch(in: "04/15/2025") {
let (whole, month, day, year) = match.output
print("Full date:", whole) // "04/15/2025"
print("Month:", month) // "04"
print("Day:", day) // "15"
print("Year:", year) // "2025"
}
} catch {
print(error)
}
TryCapture
This not only saves the matched substring but also attempts to transform it. If the transform fails, it backtracks as if the capture didn’t match.
import RegexBuilder
do {
let dateCapture = Regex {
// Month: Convert to Int + validate (1-12)
TryCapture {
Repeat(count: 2) { .digit }
} transform: { (str: Substring) -> Int? in
guard let num = Int(str), (1...12).contains(num)
else { return nil }
return num
}
"/"
// Day: Convert to Int + validate (1-31)
TryCapture {
Repeat(count: 2) { .digit }
} transform: { (str: Substring) -> Int? in
guard let num = Int(str), (1...31).contains(num)
else { return nil }
return num
}
"/"
// Year: Convert to Int + validate (1900-2100)
TryCapture {
Repeat(count: 4) { .digit }
} transform: { (str: Substring) -> Int? in
guard let num = Int(str), (1900...2100).contains(num)
else { return nil }
return num
}
}
if let match = try dateCapture.firstMatch(in: "04/15/2025") {
let (wholeMatch, month, day, year) = match.output
print("Full date:", wholeMatch) // "04/15/2025"
print("Month:", month) // 4 (Int)
print("Day:", day) // 15 (Int)
print("Year:", year) // 2025 (Int)
}
} catch {
print(error)
}
Reference
A reference to a captured portion of a regular expression. You can use a Reference to access a regular expression, both during the matching process (backreferencing) and after a capture has been successful.
import RegexBuilder
do {
let monthRef = Reference<Int>()
let dayRef = Reference<Int>()
let yearRef = Reference<Int>()
let dateRegex = Regex {
TryCapture(as: monthRef) {
Repeat(count: 2) { .digit }
} transform: { str in
Int(str).flatMap { (1...12).contains($0) ? $0 : nil }
}
"/"
TryCapture(as: dayRef) {
Repeat(count: 2) { .digit }
} transform: { str in
Int(str).flatMap { (1...31).contains($0) ? $0 : nil }
}
"/"
TryCapture(as: yearRef) {
Repeat(count: 4) { .digit }
} transform: { str in
Int(str).flatMap { (1900...2100).contains($0) ? $0 : nil }
}
}
if let match = try dateRegex.firstMatch(in: "04/15/2025") {
// Access captured values using references
print("Month:", match[monthRef]) // 4
print("Day:", match[dayRef]) // 15
print("Year:", match[yearRef]) // 2025
}
} catch {
print(error)
}
So that was all about RegexBuilder in Swift. Experiment by mixing and matching these pieces and Swift’s compiler will guide you to a correct, type-safe regex every step of the way. If you want to dig even futher here is the official documentation. I hope you enjoyed this one. I will see you in the next one. 😉