Skip to Content
Course content

76: NSRegularExpression vs Swift Regex

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was reviewing a PR for a junior dev who was implementing a simple email validation check. The code worked in the happy path, but the moment a user entered a string with an emoji, the app crashed. The culprit? NSRegularExpression. He was calculating a range using NSRange and then trying to apply that range directly to a Swift String. Because Swift strings are not just arrays of characters—they're collections of extended grapheme clusters—the indices didn't align. It's a classic trap that almost every Swift developer has fallen into at least once.

The Friction of NSRegularExpression

For a long time, NSRegularExpression was our only real option. It's essentially a wrapper around the ICU library, which means it speaks the language of Objective-C. When you use it, you aren't working with Swift's native string indices; you're working with NSRange. Converting between the two requires a fair amount of boilerplate, usually involving utf16 views, which feels clunky and error-prone.

// The old, verbose way
let pattern = "[0-9]{5}"
let regex = try! NSRegularExpression(pattern: pattern)
let text = "Order number 12345"
let range = NSRange(text.startIndex..., in: text)

if let match = regex.firstMatch(in: text, options: [], range: range) {
    if let swiftRange = Range(match.range, in: text) {
        let result = text[swiftRange]
        print("Found: \(result)")
    }
}

Notice how much "ceremony" is involved here. You have to manually handle the NSRange, cast it back to a Swift Range, and deal with multiple layers of optional unwrapping. It's a lot of noise for a very simple operation.

Strongly Typed Regex Literals

Starting with Swift 5.7, we got the Regex type and, more importantly, regex literals. Now, you can wrap your pattern in forward slashes (/.../), and the compiler actually checks your regex at compile time. If you forget a closing parenthesis or misplace a quantifier, you'll get a compiler error instead of a runtime crash. This is a massive win for developer productivity.

Beyond the syntax, the biggest improvement is the integration with Swift's type system. When you use capturing groups, Swift can return the matches as a tuple of strings (or other types), removing the need to manually slice the original string using ranges.

// The modern Swift way
let text = "Order number 12345"
if let match = text.firstMatch(of: /[0-9]{5}/) {
    print("Found: \(match.0)")
}

// Even better: Named Captures
let logEntry = "ERROR: Database timeout at 10:45AM"
let regex = /^(?[A-Z]+): (?.+)$/

if let match = logEntry.firstMatch(of: regex) {
    print("Level: \(match.level)")
    print("Message: \(match.message)")
}

I love how the named captures work here. The compiler generates a type that knows exactly which names you defined in the pattern. You get autocomplete for match.level and match.message, which makes the code significantly more readable and maintainable.

Choosing the Right Tool

You might be wondering if NSRegularExpression is completely obsolete. Not quite, but it's close. You'll still need it if you're targeting very old versions of iOS or macOS that don't support Swift 5.7+. Additionally, if you need to build a regex dynamically from user input at runtime, Regex(pattern: ) is the way to go, but the underlying engine is still the modern one.

My rule of thumb is simple: if you're on a modern toolchain, never touch NSRegularExpression unless you have a very specific legacy requirement. The safety of compile-time checking and the elimination of NSRange bugs make the new Regex type a no-brainer.




📋 Practical Task

Building a Product SKU Parser

You are working on an inventory app. Product SKUs follow a strict format: three uppercase letters, a hyphen, four digits, and a trailing single letter (e.g., "APP-1234-B" or "TSH-9021-S").

Your task is to write a function called parseSKU(_ input: String) that uses a Swift Regex literal to validate the SKU and extract the category (the first three letters) and the size (the final letter) into a tuple.

  • If the SKU is valid, return a tuple: (category: String, size: String).
  • If the SKU is invalid, return nil.

Requirements:

  • Use a regex literal (/.../).
  • Use named captures for category and size to keep the code clean.
  • Test your function with "LAP-5566-M" (should succeed) and "lap-5566-m" or "LAP-556-M" (should fail).
Rating
0 0

There are no comments for now.

to be the first to leave a comment.