Chapters

Hide chapters

Swift Apprentice: Fundamentals

First Edition · iOS 16 · Swift 5.7 · Xcode 14.2

Section III: Building Your Own Types

Section 3: 9 chapters
Show chapters Hide chapters

10. Regex
Written by Ehab Amer

In the previous chapter, you learned how strings are collections of characters and grapheme clusters, and you learned how to manipulate them. You also learned how to find a character inside a string. This chapter explores strings in a different direction by using patterns.

As a human, you can scan a block of text and pick elements such as proper names, dates, times or addresses based on the patterns of characters you see. You don’t need to know the exact values in advance to find elements.

What Is a Regular Expression?

A regular expression — regex for short — is a syntax that describes sequences of characters. Many modern programming languages, including Swift from Swift 5.7, can interpret regular expressions. In its simplest form, a regular expression looks much like a string. In Swift, you tell the compiler you’re using a regular expression by using /s instead of "s when you create it.

let searchString = "john"
let searchExpression = /john/

The code above defines a regular expression and a String you can use to search some text to find whether it contains “john”. Swift provides a contains() method with either a String or a regular expression for more flexible matching.

let stringToSearch = "Johnny Appleseed wants to change his name to John."

stringToSearch.contains(searchString) // false
stringToSearch.contains(searchExpression) // false

It might surprise you that .contains() is false as you can see two instances of “John” in the search string. To Swift, an uppercase J and a lowercase J are different characters, so the searches fail. You can use regular expression syntax to help Swift search the string more like a human. Try this expression:

let flexibleExpression = /[Jj]ohn/
stringToSearch.contains(flexibleExpression) // true

The regular expression above defines a search that begins with either an uppercase or a lowercase J followed by the string “ohn”. Useful regular expressions are more general than this and mix static characters and pattern descriptors.

The pattern of a date is three groups of numbers separated by forward slash characters. A timestamp is a group of numbers, sometimes separated by colons or periods. Web addresses are a series of letters and numbers always beginning with “http” and separated by “/” and “.” and “?”. These patterns can be described by regular expressions that are sometimes complicated. But you’ll start with something simple and work your way up.

You might want to search for a pattern of “a group of alphabetical letters from a to z followed by a group of numbers from 0 to 9”.

One way to represent this in regular expression syntax is /[a-z]+[0-9]+/.

This expression will match text including abcd12345 or swiftapprentice2023 but won’t work on XYZ567 or Pennsylvania65000. The expression only describes lowercase ASCII, not uppercase.

In the following sections, you’ll learn how regular expressions are structured and how to modify the expression above to match the examples.

Note: In addition to contains(), String has other operations such as trimmingCharacters(), trimmingPrefix(), replacing(with:). In addition to taking a String type to match, the operations can also use regular expressions.

Regular Expression Structure

A regular expression mainly consists of two parts:

  1. Something to describe which character group you’re searching for.
  2. A repetition form of that character group.

The example above [a-z] describes a single character from a to z. It’s followed by + to show it’s repeated one or more times. Then the range of digits from 0 to 9 repeats one or more times.

You can see that you concatenate two expressions to form a more complex expression.

Character Representations

Remember that regular expressions are a syntax for defining patterns. Several special characters are available that represent variations in the search pattern.

  • \d: Any single character that’s a digit. It can replace [0-9].
  • \w: Any single character that’s part of the ASCII letters or numbers. It can replace [a-zA-Z0-9_].
  • \s: Any single character representing whitespace, such as a space or a tab.

You can also express the inverse of each set using the uppercased representation.

  • \D: Anything that is not a digit.
  • \W: Anything that is not an alphabet character, a digit or an underscore.
  • \S: Anything that is not whitespace.

Other pattern descriptor symbols give you more control over the matched character.

  • [ - ]: A range of characters or numbers. You don’t need to specify the whole alphabet or all digits; you can specify the range you want, like [m-r] or [4-8].
  • [ ]: A set of characters to choose from. For example, [AEIOUaeiou] will match vowel characters in uppercase or lowercase.
  • |: The or operator can do the same as the above vowel example, like A|E|I|O|U|a|e|i|o|u. It’s more commonly seen with multicharacter expressions like searching for different endings to words s|ed|ing.

Note: The range [ - ] representation works with the universal character table. That table has entries for all characters, including accented characters, symbols and emojis. For example, [5-d] is a valid range that includes the digits five through nine, all uppercase letters, the first few lowercase letters, and some math symbols. It’s unlikely you’d ever make a range that spans character groupings on purpose.

The magic . character matches any character. Be careful using it because it might create matches you don’t expect.

Using any of the symbols above matches a single character. If you apply the expression [a-d] to the string abcdefghijk, it’ll match each of the four characters a, b, c and d on their own and return four results. If you want a single result with the string abcd, you must have some repetition in the expression.

Repetitions

You have already used the repetition descriptor + for one or more. Multiple types of repetition descriptors exist. They follow the character pattern you want to repeat:

  • +: The pattern appears one or more times. The expression [a-z]+ matches one or more lowercase letters in a row.
  • ?: The pattern can appear once or not appear. An expression of [a-z]?[0-9]+ matches numbers only or a single lowercase character followed by numbers.
  • *: The pattern appears zero or more times. An expression of [a-z]*[0-9]+ matches numbers only or one or more lowercase letters followed by numbers.
  • {n,} The pattern repeats a minimum n times. The + above can also be represented as {1,} and * is the same as {0,}.
  • {n,m} The pattern repeats minimum n times and a maximum of m times.

Mini-Exercise

Now that you’ve learned to construct regular expressions with different capabilities, how would you adapt /[a-z]+[0-9]+/ from earlier to match all of the example texts abcd12345, swiftapprentice2023, XYZ567, Pennsylvania65000?

Compile Time Checking

What separates Swift regex from other languages (and earlier versions of Swift before 5.7) is its ability to check for correctness at compile time. Consider the following:

let lowercaseLetters = /[a-z*/

Rather than let this go and fail to match at runtime, the Swift compiler prevents the problem entirely with the error:

You can fix it by adding the missing ] character:

let lowercaseLetters = /[a-z]*/

Wherever it can, the Swift compiler keeps you on the right track by ensuring your regexes are well-formed expressions.

Regular Expression Matches

Regular expression matches can sometimes be surprising. To explore kinds of matching, start with this example:

let lettersAndNumbers = /[a-z]+[0-9]+/

You saw how to use .contains() to see whether a match exists in a string, but it’s more powerful to use the method String.matches(of:) to find the matched results of the expression within a string. The .matches(of:) provides the matched characters and where they appear in the original string using .range. You worked with Range types in the previous chapter, “Strings”:

let testingString1 = "abcdef ABCDEF 12345 abc123 ABC 123 123ABC 123abc abcABC"
for match in testingString1.matches(of: lettersAndNumbers) {
 print(String(match.output))
}

The code above will print this output to the console:

abc123

Now, change the repetition modifier to explore various ways matching works. The * will match each character zero or more times.

let possibleLettersAndPossibleNumbers = /[a-z]*[0-9]*/

When using possibleLettersAndPossibleNumbers with testingString1, you might expect that this will give three matches:

  • Lowercase letters only abcdef.
  • Numbers only 12345 and 123.
  • Lowercase letters then numbers abc123.

However, when you execute this code in the playground, you’ll get many more matches.

for match in testingString1.matches(of: possibleLettersAndPossibleNumbers) {
  print(String(match.output)) // 32 times
}

How is that possible?

As Swift compares your regular expression to the string, it considers all possibilities. Looking at the expression above, zero or more is possible for each part. Meaning that zero for both is a valid option. In other words, this expression can match all possible empty ranges of the string.

Explore matching an empty string using the code below.

let emptyString = ""
let matchCount = emptyString.matches(of:
                    possibleLettersAndPossibleNumbers).count // 1

The value of matchCount isn’t zero. An actual match is found within an empty string because the empty string contains a pattern you describe: zero letters followed by zero numbers. This result is called a zero-length match.

Avoiding Zero-Length Matches

The regular expression engine starts at a position in the search string and increments along as far as it can while still matching the expression. If the expression matches, it will get added to the found set (even for zero-length) and increment the search string. This repeats until the search string is consumed.

Note: The engine will never use the same search position twice to avoid infinite loops.

When you design your expressions, avoid situations where nothing is a match.

An expression that doesn’t allow zero-length matches would look like this:

let fixedPossibleLettersAndPossibleNumbers = /[a-z]+[0-9]*|[a-z]*[0-9]+/

This expression uses the | or operator. It describes a pattern of either one or more letters followed by a group of numbers, or a group of letters followed by one or more numbers. Either side of the or is guaranteed to contain at least one character, a letter or a number. So this expression will never match nothing.

for match in testingString1.matches(of: fixedPossibleLettersAndPossibleNumbers) {
  print(String(match.output))
}

Running the expression against the sample string will give the following results:

abcdef
12345
abc123
123
123
123
abc
abc

Although this is better, you’re probably expecting four results. The expression matched with eight results instead. Looking at the original string, you’ll see the matches in the curly braces: “{abcdef} ABCDEF {12345} {abc123} ABC {123} {123}ABC {123}{abc} {abc}ABC”

Suppose the first four results are the ones you’re expecting, but the last four are not.

If you compare the matched strings against your expression, you’ll notice that, unfortunately, they match. You want to extract words, not parts of a word, but that’s different from what your expression describes.

Result Boundaries

One way to solve this is to specify boundaries that should contain each result. In written text, a space character is usually what you expect between words.

Just as it’s easier to use \w instead of [a-zA-Z0-9_], you can specify word boundaries using \b. This special descriptor takes care of the corner cases that crop up because of Unicode.

let fixedWithBoundaries = /\b[a-z]+[0-9]*\b|\b[a-z]*[0-9]+\b/

This version adds the boundary character \b, known as an anchor, to each side of the two expressions around the or operator |.

for match in testingString1.matches(of: fixedWithBoundaries) {
  print(String(match.output))
}

Now you’ll finally see the four results you’re expecting:

abcdef
12345
abc123
123

Note: Regular expressions also understand that lines of text have a beginning and an end. The anchor character^ will ensure that a match only happens at the beginning of a line, while the anchor $ will only match at the end.

Challenge 1

Create a regular expression that matches any word that contains a sequence of two or more uppercase characters. Examples: 123ABC - ABC123 - ABC - abcABC - ABCabc - abcABC123 - a1b2ABCDEc3d4. It should reject abcA12a3 - abc123.

Test on the sample strings provided above.

Hints: A range expression can contain many range sets. [a-z0-9] can match a lowercase letter or a number. [a-z0-9]+ can match a repetition with a mix of both like a2c456xyz. You can use {2,} two or more.

A Better Way to Write Expressions

So far, you’ve been writing regular expressions using the standard syntax. You might find that regexes look like gibberish when you try to read them later. Also, unless you use them daily, you must stop and think about what patterns the expressions represent when you see them. Don’t worry — it’s a common problem. :]

Swift’s new Regex type also introduces a friendlier and more readable way to design expressions. Writing expressions in this manner makes it easier to remember what they represent when you return to the code later. This new syntax also makes it possible to leverage code completion to get at pattern descriptors used by your expression. As before, the compiler can provide compile-time diagnostics to help avoid mistakes.

First, add this import to the top of your Swift file:

import RegexBuilder

Using the first regular expression example from earlier, [a-z]+[0-9]+, translate it to the new syntax like this:

let newlettersAndNumbers = Regex {
  OneOrMore { "a"..."z" }
  OneOrMore { .digit }
}

This expression is identical to the compact expression /[a-z]+\d+/ but written more clearly.

Swift provides several operators and constants to represent the special regular expression commands. Here is the table from earlier in the chapter repeated with the equivalents in RegexBuilder:

  • \d = CharacterClass.digit.

  • \w = CharacterClass.word.

  • \s = CharacterClass.whitespace.

  • \D = CharacterClass.digit.inverted.

  • \W = CharacterClass.word.inverted.

  • \S = CharacterClass.whitespace.inverted.

  • . = CharacterClass.any.

  • \b = Anchor.wordBoundary.

  • [ - ] = Directly using a range of characters like "m"..."r" or "4"..."8".

  • [ ] = CharacterClass.anyOf("AEIOUaeiou").

  • | = ChoiceOf { } This is more convenient for longer expressions.

  • + = OneOrMore { }.

  • ? = Optionally { }.

  • * = ZeroOrMore { }.

  • {n,} = Repeat(n...) { }.

  • {n,m} = Repeat(n...m) { }.

You can represent the expression \b[a-z]+[0-9]*\b|\b[a-z]*[0-9]+\b with RegexBuilder like this:

let newFixedRegex = Regex {
  Anchor.wordBoundary
  ChoiceOf {
   Regex {
     OneOrMore {
       "a"..."z"
     }
     ZeroOrMore {
       .digit
     }
   }
   Regex {
     ZeroOrMore {
       "a"..."z"
     }
     OneOrMore {
       .digit
     }
   }
  }
  Anchor.wordBoundary
}

This time, the wordBoundary is present outside of the or operator ChoiceOf. You can control the groupings that fall within the ChoiceOf block.

Note: The prefix CharacterClass is part of the full name. Usually, in your code, you can use the class names, and the compiler uses type inference to figure out the rest. Instead of CharacterClass.digit, you’ll likely use .digit. If the compiler ever complains that it doesn’t know about the CharacterClass or any other RegexBuilder commands, check that you have added import RegexBuilder to the top of the file.

Challenge 2

Update the expression you created in Challenge 1 to use the new RegexBuilder structure and match expressions that have multiple sequences of uppercase characters. Example a1b2ABCDEc3d4FGHe5f6g7

Hint: You can represent the expression [a-z0-9] by creating a union between two character classes. E.g., CharacterClass.digit.union("a"..."z").

Refactoring to RegexBuilder

As you complete Challenge 2, you might wonder how this new way is better. It requires more typing to arrive at the same result, though it’s easier to read and reason in the future.

Apple recognized there might be many complex regular expressions already in code. So, in Xcode, a refactor option takes any regular expression in your code and converts it to RegexBuilder format. Give it a try.

Place the cursor in any part of a regular expression definition in your code and right-click to reveal the contextual menu. Select Refactor ▸ Convert to Regex Builder.

Also, you can refactor from the main Editor menu.

Capturing Results

So far, you’ve used regular expressions to match a pattern in a larger string. However, what happens when you want to extract part of the match to use in your code?

You might remember from earlier in the chapter that the .matches(of:) output will have the range of the match. So, it would certainly be possible to write some code to use that Range type to traverse the string and pull out the match. Then use some more code to convert the string to a different data type, like an Int. That’s a lot of extra work.

Thankfully, regular expressions have something called Captures that allows you to assign parts of the result to special variables. With Swift, you can even name the variables in the regular expression and convert the captured value from String to something else you can use in your code.

Using the regular expression syntax, place any descriptors you want to capture from a longer expression by surrounding them in parenthesis (). The expression to capture digits from inside groups of letters would look like: [a-z]+(\d+)[a-z]+.

Or, using RegexBuilder, the expression would look like this:

let regexWithCapture = Regex {
  OneOrMore {
    "a"..."z"
  }
  Capture {
    OneOrMore {
      CharacterClass.digit
    }
  }
  OneOrMore {
    "a"..."z"
  }
}

When you use a capture, the type of the output changes from Substring to a tuple (Substring, Substring). Each capture will make the tuple longer to include it. An expression with two captures will have a tuple of three items, and an expression with five captures will have a tuple of six. The first item in the tuple is always the full match of the expression. (FullMatch, Capture1, Capture2, …)

let testingString2 = "welc0me to chap7er 10 in sw1ft appren71ce. " +
  "Th1s chap7er c0vers regu1ar express1ons and regexbu1lder"

for match in testingString2.matches(of: regexWithCapture) {
  print(match.output)
}

The printed result of this code is:

("elc0me", "0")
("chap7er", "7")
("sw1ft", "1")
("appren71ce", "71")
("h1s", "1")
("chap7er", "7")
("c0vers", "0")
("regu1ar", "1")
("express1ons", "1")
("regexbu1lder", "1")

You can also assign the tuple to named variables using a let statement with the output. For the strings above, you might use something like:

for match in testingString2.matches(of: regexWithCapture) {
  let (wordMatch, extractedDigit) = match.output
  print("Full Match: \(wordMatch) | Captured value: \(extractedDigit)")
}

This code prints:

Full Match: elc0me | Captured value: 0
Full Match: chap7er | Captured value: 7
Full Match: sw1ft | Captured value: 1
Full Match: appren71ce | Captured value: 71
Full Match: h1s | Captured value: 1
Full Match: chap7er | Captured value: 7
Full Match: c0vers | Captured value: 0
Full Match: regu1ar | Captured value: 1
Full Match: express1ons | Captured value: 1
Full Match: regexbu1lder | Captured value: 1

The digits in the tuple captured from the string are also represented as strings. You can use a TryCapture command to manipulate the match with a transform closure to change the data type.

let regexWithStrongType = Regex {
  OneOrMore {
    "a"..."z"
  }
  TryCapture {
    OneOrMore {
      CharacterClass.digit
    }
  } transform: {foundDigits in
     Int(foundDigits)
  }
  OneOrMore {
    "a"..."z"
  }
}

The code above has replaced Capture with TryCapture. When the TryCapture is successful, it passes the matched string into the transform closure. The code in the closure converts the matched string into an Int type.

Now when you execute code to output the matching tuples, instead of a tuple with two String types, you see a String type and an Int type.

("elc0me", 0)
("chap7er", 7)
("sw1ft", 1)
("appren71ce", 71)
("h1s", 1)
("chap7er", 7)
("c0vers", 0)
("regu1ar", 1)
("express1ons", 1)
("regexbu1lder", 1)

You must be aware of something critical when using captures. You won’t have multiple captures, even if your capture is inside a repetition. The capture will store the last found value and not all of the other matches.

Consider this example string:

let repetition = "123abc456def789ghi"

You want to capture the numbers found in the above string, not the letters. Your expression might be:

let repeatedCaptures = Regex {
  OneOrMore {
    Capture {
      OneOrMore {
        CharacterClass.digit
      }
    }
    OneOrMore {
      "a"..."z"
    }
  }
}

You would expect that the matches will include 123, 456 and 789:

for match in repetition.matches(of: repeatedCaptures) {
  print(match.output)
}

The output from this code is: ("123abc456def789ghi", "789"). The expression has only one capture block. It doesn’t matter if it’s inside a repetition that iterates only once or a hundred times. The value stored is the one found in the last iteration.

Challenge 3

Change the expression used in the last challenge to capture the text in uppercase. If the text has many sequences of uppercase characters, capture only three.

Key Points

  • Regular expressions give you incredible flexibility for matching patterns over simple substring matching.
  • Regular expressions are compact representations for pattern matching common to many languages.
  • Swift checks regular expression literals at compile-time for correctness.
  • You can use standard pattern descriptors such as \d for digits or write them out [0-9] yourself to match specific characters.
  • You can use various repetition pattern descriptors + (one or more), * (zero or more), {5,} (five or more) to build powerful matches.
  • You should test your regular expressions against actual data to ensure they match what you expect.
  • Boundary anchors like ^ (beginning of a line), $ (end of a line) and \b (word) can narrow down the results to words or lines and avoid zero-length matches.
  • Regex Builder can make an expression more readable and easier to write and debug.
  • You can capture one or more parts of a matched expression.
  • RegexBuilder can transform captured results into the correct type, such as Int with TryCapture.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.