let regex = /^[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/; return (try? regex.wholeMatch(in: email)) != nil. For backward compatibility, evaluate the string using NSPredicate(format: "SELF MATCHES %@", pattern). Client-side validation should only ensure basic structural conformance before sending a verification link.Why this happens
Validating an email address inside client-side iOS or macOS code is tricky because the formal specification (RFC 5322) is notoriously complex. RFC-compliant email formats support characters, quoted strings, IP domain literals, and nested comments that almost no practical consumer application accepts or expects.
Developers often run into two extremes:
- Overly permissive checks, such as simply checking
email.contains("@"), which allow obvious typos likeuser@domain(missing top-level domain) or@domain.com(missing local part) to pass into your registration pipeline. - Overly strict regular expressions, such as restricting the top-level domain to 2–4 characters (
[A-Za-z]{2,4}), which reject modern generic top-level domains like.engineering,.photography, or country codes like.museum.
Bridging legacy Objective-C regular expressions into Swift also introduces friction. In early Swift code, developers relied on NSPredicate or NSRegularExpression, both of which require manual bridge casting to NSString and awkward UTF-16 range computations. Modern Swift offers native regular expressions that eliminate these Foundation wrappers.
How to fix it
1. Use Swift Native Regex (iOS 16+ / macOS 13+)
Starting with Swift 5.7 and iOS 16, Swift provides native regular expression literals and the Regex engine directly in the standard library. This avoids Objective-C bridging overhead and provides compile-time regex syntax checks.
Use wholeMatch(in:) to ensure the pattern spans the entire string from start to finish without needing explicit ^ and $ boundary anchors:
import Foundation
struct EmailValidator {
// Pattern permits standard alphanumeric characters, common symbols in the local part,
// domain labels separated by dots, and a top-level domain of at least 2 characters.
private static let emailRegex = /[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/
static func isValid(_ email: String) -> Bool {
let trimmed = email.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else {
return false
}
return (try? emailRegex.wholeMatch(in: trimmed)) != nil
}
}
You can also integrate this check directly into a SwiftUI form binding or view model:
import SwiftUI
@Observable
final class SignUpViewModel {
var email: String = ""
var isEmailValid: Bool {
let trimmed = email.trimmingCharacters(in: .whitespacesAndNewlines)
let regex = /^[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/
return (try? regex.wholeMatch(in: trimmed)) != nil
}
}
2. Use NSPredicate with Regular Expressions (Legacy iOS Support)
If your deployment target includes iOS 15 or earlier, NSPredicate provides a clean, concise syntax that wraps regex matching through Foundation without manual range arithmetic.
Extend String to make validation accessible across your project:
import Foundation
extension String {
var isValidEmail: Bool {
let trimmed = self.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else {
return false
}
// Top-level domain allows between 2 and 64 characters to support modern TLDs
let emailFormat = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
let emailPredicate = NSPredicate(format: "SELF MATCHES %@", emailFormat)
return emailPredicate.evaluate(with: trimmed)
}
}
Usage:
let input = "developer@apple.com"
if input.isValidEmail {
print("Valid email structure")
} else {
print("Invalid email structure")
}
Note on formatting: Because the pattern is evaluated through NSPredicate, the period separating the domain name and TLD must be escaped with double backslashes (\\.) to avoid being interpreted as any character.
3. Use NSDataDetector for RFC-Compliant Link Extraction
If you prefer relying on Apple’s built-in linguistic and data detection engines rather than maintaining a custom regex, use NSDataDetector. It detects URLs, phone numbers, and mailto: links using Apple’s system parser:
import Foundation
struct SystemEmailValidator {
private static let detector = try? NSDataDetector(
types: NSTextCheckingResult.CheckingType.link.rawValue
)
static func isValid(_ email: String) -> Bool {
let trimmed = email.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty, let detector = detector else {
return false
}
let range = NSRange(trimmed.startIndex..<trimmed.endIndex, in: trimmed)
let matches = detector.matches(in: trimmed, options: [], range: range)
// The string must contain exactly one match, spanning the full input, with a mailto scheme
guard let firstMatch = matches.first,
matches.count == 1,
firstMatch.range.location == 0,
firstMatch.range.length == range.length,
let url = firstMatch.url,
url.scheme == "mailto" else {
return false
}
return true
}
}
This method conforms closely to Apple’s system behaviors, but it can be more permissive than typical sign-up forms expect (for example, allowing addresses without standard dots if the local network configuration allows it).
How to prevent it
- Trim whitespace first: Users frequently paste email addresses containing leading or trailing whitespace. Call
trimmingCharacters(in: .whitespacesAndNewlines)before running validation. - Do not limit TLD length to 3 or 4 characters: Avoid old regex patterns like
\.[A-Za-z]{2,4}. Use\.[A-Za-z]{2,}or{2,64}to accommodate new generic top-level domains like.technologyor.foundation. - Support plus-addressing: Ensure the pattern allows the
+character in the local part (user+newsletter@example.com), which users often employ for filtering. - Treat client validation as UX, not security: Regex checks prevent immediate typos, but only an emailed confirmation token proves that the domain exists and the user owns the inbox.
Parts of this guide are adapted from content licensed under CC BY-SA 4.0; this article is shared under the same license.
