Regular expressions in Swift

How to work with them?

published on February 27, 2025

Regular expressions, those cryptic strings of characters that can make even seasoned developers break out in a cold sweat. For years, regex has been both a powerful tool and a daunting puzzle, its complexity often leaving programmers wary of diving too deep. But with Swift 5.7 introduced a set of features that changes this forever. With its new regex features, Swift is stepping up to tame the beast, offering a fresh, approachable way to harness the power of pattern matching without the usual headaches. In this post, we’ll explore how Swift’s latest reflex enhancements are making regex less of a nightmare and more of a dream for developers everywhere.

What is regex?

Regular expressions, or regex, are patterns used to match, search, and manipulate text. They consist of special characters and sequences that define rules for identifying specific strings or patterns within larger bodies of text. Think of them as a super-flexible search tool that can find anything from exact words to complex combinations like email addresses or phone numbers. While they can seem cryptic at first, regex is widely used in programming for tasks like validation, text parsing, and data extraction.

Let's have a look at two example regexes

1. ^\+(\d{1,3})[-. ]?(\d{1,4})[-. ]?(\d{2,4})[-. ]?(\d{2,4})$
2. [a-z]{3}

If you have never seen a regex before, looking at the first example you may start to realise why many people fear them. Of course with the introduction of AI coding agents, tackling regexes become a lot more approachable but yet, its always useful to know them. Let’s see how Swift 5.7 changed the way we can work with regexes

While this article is not a regex tutorial, for the sake of clarity, let’s mention that the first regex matches international phone numbers while the second one matches 3 letter words

The regex object

At the core of Swift’s regex handling lies the Regex object. It’s straightforward to use: instantiate it with a pattern and apply it to search text.

let pattern = try Regex(“[a-z]{3}”)
let matches = text.ranges(of: pattern)

This works, but it has a catch. Because the regex string is parsed at runtime, the try keyword is required, and error, like a malformed pattern, only surface when the code executes. This delay in feedback can slow development and introduce runtime risks.

The regex literal

Enter the regex literal, a game-changer in Swift’s regex arsenal. Here’s how it looks:

let regex = /[a-z]{5} (\d+)/
let result = try? regex.wholeMatch(in: "hello 12")

print(result?.1)

Unlike the Regex object, regex literals are parsed at compile time. This brings two significant advantages: early pattern validation catches errors during development, and Swift’s type safety integrates seamlessly with the pattern. Consider this enhancement:

let regex = /[a-z]{5} (?‹age>\d+)/
let result = try? regex.wholeMatch(in: "hello 12")

print(result?.age)

Named groups are nothing new in regexes, but being able to reference the names used in a regex pattern at compile time in the rest of the code simply wouldn't be possible with a runtime solution. This feature brings working with regex matches to a whole new level of clarity and safety.

The regex DSL

Our final option is just as exciting as the previous ones. The regex Domain-Specific Language (DSL) provides a descriptive way of constructing regular expressions using a syntax that resembles SwiftUI. The DSL implementation relies on result builders and represents an excellent example of how to use them outside of SwiftUI. Let's see how our previous example would look:

Import RegexBuilder

let age = Reference(Substring.self)
let regex = Regex {
    Repeat(count: 5) {
        ("a"..."z")
    }
    " "
    Capture(as: age) {
        OneOrMore(.digit)
    }
}

let result = try? regex.wholeMatch(in: "hello 12")
print(result?[age])

As you can see, this syntax is highly descriptive, making it easy to understand the purpose of the regex even for those unfamiliar with traditional regular expression syntax. Furthermore, the DSL makes it simple to convert matches to custom data types in place, eliminating the need to extract a string match and then convert it.

let age = Reference(Int.self)

//…

TryCapture(as: age) {
        OneOrMore(.digit)
} transform: { match in
        Int(match)
}

TryCapture ensures not only that result?[age] will have a type of Int but also handles rejecting the match if the group matched is not convertible to Int, that is when Int(match) returns nil.

This approach may seem too verbose, I personally prefer the regex literal for simple tasks, however its true power becomes evident when we take a look again at the first regex example from this article

\+(\d{1,3})[-. ]?(\d{1,4})[-. ]?(\d{2,4})[-. ]?(\d{2,4})

This challenging regex is difficult to decode, not necessarily because of individual rules but due to its length and complexity. Here's how it would look using the regex builder:

let pattern = Regex {
    "+"
    Capture {
        Repeat(1...3) {
            One(.digit)
        }
    }
    Optionally(CharacterClass.anyOf("-. "))
    Capture {
        Repeat(1...4) {
            One(.digit)
        }
    }
    Optionally(CharacterClass.anyOf("-. "))
    Capture {
        Repeat(2...4) {
            One(.digit)
        }
    }
    Optionally(CharacterClass.anyOf("-. "))
    Capture {
        Repeat(2...4) {
            One(.digit)
        }
    }
}

Reading this structured code should be much more approachable for anyone, making the regex DSL a perfect fit for simplifying complex regex patterns.

💡Note: If you already have a regex and you want to convert it into a regex builder, Xcode can easily do it for you. Just highlight the pattern, right click on it and choose Refactor > Convert to regex builder

Conclusion

Swift 5.7's regex enhancements represent a significant leap forward in making regular expressions more accessible and maintainable for developers. Through the introduction of three complementary approaches, the traditional Regex object, the compile-time validated regex literal, and the expressive Regex DSL, Swift has transformed what was once a cryptic, error-prone aspect of programming into a safer, more intuitive experience.

The regex literal brings compile-time safety to pattern validation, eliminating many runtime issues while enabling the power of named capture groups with Swift's type system. Meanwhile, the Regex DSL provides unprecedented readability for complex patterns through its declarative syntax, making even the most intricate matching logic comprehensible at a glance.

These innovations don't merely make regex easier to write, they fundamentally change how developers interact with pattern matching in their code. Complex text processing tasks that once required careful documentation and cautious testing can now be expressed clearly in code that practically documents itself. The ability to transform matched text directly into strongly-typed Swift objects further bridges the gap between pattern matching and Swift's type safety.

As developers embrace these new tools, we can expect to see more confident and creative uses of regular expressions in Swift applications. What was once a necessary evil becomes a powerful ally, allowing for more robust input validation, more sophisticated text processing, and ultimately more reliable software. Swift's regex revolution proves that even the most intimidating aspects of programming can be reimagined to become more accessible without sacrificing power or flexibility.

Follow me on X