Articles, podcasts and news about Swift development, by John Sundell.

Validating email addresses using RawRepresentable and NSDataDetector

Published on 08 Jan 2021

These days, it’s incredibly common for apps to have to work with email addresses one way or another, and when doing so, we typically want to perform some kind of client-side validation on any addresses that were entered by the user.

Conceptually, performing that kind of validation should be quite simple, given that email addresses need to conform to a certain format in order to be valid, but in practice, it can be quite difficult to figure out exactly how to implement that kind of logic in Swift.

One very common way to do so is to use a regular expression, which when combined with NSPredicate lets us validate a given email address String like this (the actual regular expression has been omitted for brevity):

class SignUpViewController: UIViewController {
    private lazy var emailAddressField = UITextField()

    @objc private func handleSignUpButtonPress() {
        let emailPredicate = NSPredicate( 
            format: "SELF MATCHES %@", "<regular expression>"
        )

        guard let email = emailAddressField.text,
              emailPredicate.evaluate(with: email) else {
            return showErrorView(for: .invalidEmailAddress)
        }
        
        ...
    }
}

However, while the above technique certainly works, it does have a few downsides. First of all, we have to either carefully craft the regular expression that we wish to use ourselves, or figure out which of the many variants that can be found online that’s actually the right one to use. But perhaps more importantly is that when storing and passing email addresses as raw String values, there’s no way of establishing a guarantee that a given value has actually been validated.

For example, by just looking at the following User type, we can’t tell whether the emailAddress that is being stored within it has actually been passed through any kind of validation, since that logic is currently completely detached from that value:

struct User: Codable {
    var name: String
    var emailAddress: String
    ...
}

However, it turns out that Swift actually has a built-in pattern that could let us solve the above set of problems quite elegantly.

If we think about it, we very often deal with other kinds of validated raw values in the shape of enums. For example, the following UserGroup enum can be initialized with a raw String value, but will only actually return an instance if that raw value matched one of our enum’s cases:

enum UserGroup: String {
    case admins
    case moderators
    case regular
}

At first, it might seem like the init(rawValue:) initializer that raw value-backed enums get is the result of some kind of hard-coded compiler logic that’s specific for enums. While that’s partially true, that initializer is actually part of a general-purpose protocol called RawRepresentable, which enums that have raw values automatically conform to.

That means that we can also define our own RawRepresentable types as well, which lets us encapsulate our email validation logic in a very neat way — like this:

struct EmailAddress: RawRepresentable, Codable {
    let rawValue: String

    init?(rawValue: String) {
        // Validate the passed value and either assign it to our
        // rawValue property, or return nil.
        ...
    }
}

Note that our new EmailAddress type also conforms to Codable, which we get support for without having to write any additional code. Values will be automatically encoded and decoded to and from our underlying rawValue property, just like how enums work in that context.

Now, while we could simply copy/paste our regular expression-based validation logic from before into our new EmailAddress type, let’s also use this opportunity to explore a different (and, if you ask me, better) way of performing our validation — by using Foundation’s NSDataDetector API.

Under the hood, NSDataDetector actually uses regular expressions as well, but hides those details behind a series of dedicated APIs that let us identify tokens like links, phone numbers, and email addresses. Here’s how we could use the link checking type to extract a mailto link for the email address that we’re validating, which we could then perform a few extra checks on like this:

struct EmailAddress: RawRepresentable, Codable {
    let rawValue: String

    init?(rawValue: String) {
        let detector = try? NSDataDetector(
            types: NSTextCheckingResult.CheckingType.link.rawValue
        )

        let range = NSRange(
            rawValue.startIndex..<rawValue.endIndex,
            in: rawValue
        )

        let matches = detector?.matches(
            in: rawValue,
            options: [],
            range: range
        )
    
        // We only want our string to contain a single email
        // address, so if multiple matches were found, then
        // we fail our validation process and return nil:
        guard let match = matches?.first, matches?.count == 1 else {
            return nil
        }

        // Verify that the found link points to an email address,
        // and that its range covers the whole input string:
        guard match.url?.scheme == "mailto", match.range == range else {
            return nil
        }

        self.rawValue = rawValue
    }
}

With the above in place, we can now simply use our new EmailAddress type wherever we’re storing an email address, and we’ll get a compile time guarantee that each of those addresses will always be validated when they’re converted from a raw String value:

struct User: Codable {
    var name: String
    var emailAddress: EmailAddress
    ...
}

To then actually convert raw email address strings into instances of our new type, we can simply use the same init(rawValue:) initializer that’s used to convert raw values into enums, or in the case of our SignUpViewController from before, we could use flatMap on the optional String that UITextField gives us, and then pass our new type’s initializer as a first class function — like this:

class SignUpViewController: UIViewController {
    private lazy var emailAddressField = UITextField()

    @objc private func handleSignUpButtonPress() {
        // As an added bonus, we also trim all whitespaces from
        // the string that the user entered before validating it:
        let rawEmail = emailAddressField.text?.trimmingCharacters(
            in: .whitespaces
        )

        guard let email = rawEmail.flatMap(EmailAddress.init) else {
            return showErrorView(for: .invalidEmailAddress)
        }
        
        ...
    }
}

Of course, if we wanted to, we could’ve kept using the regular expression-based approach within our EmailAddress implementation as well, but I personally think that the combination of a dedicated RawRepresentable type and NSDataDetector results in a much simpler solution.

What do you think? Feel free to share this article if you enjoyed it, or contact me via either Twitter or email if you have any questions or comments. Thanks for reading!