How to make simple form validator in Swift

Issue #328

Sometimes we want to validate forms with many fields, for example name, phone, email, and with different rules. If validation fails, we show error message.

We can make simple Validator and Rule

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Validator {
func validate(text: String, with rules: [Rule]) -> String? {
return rules.compactMap({ $0.check(text) }).first
}

func validate(input: InputView, with rules: [Rule]) {
guard let message = validate(text: input.textField.text ?? "", with: rules) else {
input.messageLabel.isHidden = true
return
}

input.messageLabel.isHidden = false
input.messageLabel.text = message
}
}

struct Rule {
// Return nil if matches, error message otherwise
let check: (String) -> String?

static let notEmpty = Rule(check: {
return $0.isEmpty ? "Must not be empty" : nil
})

static let validEmail = Rule(check: {
let regex = #"[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,64}"#

let predicate = NSPredicate(format: "SELF MATCHES %@", regex)
return predicate.evaluate(with: $0) ? nil : "Must have valid email"
})

static let countryCode = Rule(check: {
let regex = #"^\+\d+.*"#

let predicate = NSPredicate(format: "SELF MATCHES %@", regex)
return predicate.evaluate(with: $0) ? nil : "Must have prefix country code"
})
}

Then we can use very expressively

1
2
let validator = Validator()
validator.validate(input: inputView, with: [.notEmpty, .validEmail])

Then a few tests to make sure it works

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class ValidatorTests: XCTestCase {
let validator = Validator()

func testEmpty() {
XCTAssertNil(validator.validate(text: "a", with: [.notEmpty]))
XCTAssertNotNil(validator.validate(text: "", with: [.notEmpty]))
}

func testEmail() {
XCTAssertNil(validator.validate(text: "onmyway133@gmail.com", with: [.validEmail]))
XCTAssertNotNil(validator.validate(text: "onmyway133", with: [.validEmail]))
XCTAssertNotNil(validator.validate(text: "onmyway133.com", with: [.validEmail]))
}

func testCountryCode() {
XCTAssertNil(validator.validate(text: "+47 11 222 333", with: [.countryCode]))
XCTAssertNotNil(validator.validate(text: "11 222 333", with: [.countryCode]))
XCTAssertNotNil(validator.validate(text: "47 11 222 333", with: [.countryCode]))
}
}

allSatisfy

To check if all rules are ok, we can use reduce

1
2
3
func check(text: String, with rules: [Rule]) -> Bool {
return rules.allSatisfy({ $0.check(text).isOk })
}

Or more concisely, use allSatisfy

1
2
3
4

func check(text: String, with rules: [Rule]) -> Bool {
return rules.allSatisfy({ $0.check(text).isOk })
}

Comments