(_ conditionClass: T.Type) {
51 | conditions = conditions.filter { !($0 is T) }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/docs/js/jazzy.search.js:
--------------------------------------------------------------------------------
1 | $(function(){
2 | var searchIndex = lunr(function() {
3 | this.ref('url');
4 | this.field('name');
5 | });
6 |
7 | var $typeahead = $('[data-typeahead]');
8 | var $form = $typeahead.parents('form');
9 | var searchURL = $form.attr('action');
10 |
11 | function displayTemplate(result) {
12 | return result.name;
13 | }
14 |
15 | function suggestionTemplate(result) {
16 | var t = '';
17 | t += '' + result.name + '';
18 | if (result.parent_name) {
19 | t += '' + result.parent_name + '';
20 | }
21 | t += '
';
22 | return t;
23 | }
24 |
25 | $typeahead.one('focus', function() {
26 | $form.addClass('loading');
27 |
28 | $.getJSON(searchURL).then(function(searchData) {
29 | $.each(searchData, function (url, doc) {
30 | searchIndex.add({url: url, name: doc.name});
31 | });
32 |
33 | $typeahead.typeahead(
34 | {
35 | highlight: true,
36 | minLength: 3
37 | },
38 | {
39 | limit: 10,
40 | display: displayTemplate,
41 | templates: { suggestion: suggestionTemplate },
42 | source: function(query, sync) {
43 | var results = searchIndex.search(query).map(function(result) {
44 | var doc = searchData[result.ref];
45 | doc.url = result.ref;
46 | return doc;
47 | });
48 | sync(results);
49 | }
50 | }
51 | );
52 | $form.removeClass('loading');
53 | $typeahead.trigger('focus');
54 | });
55 | });
56 |
57 | var baseURL = searchURL.slice(0, -"search.json".length);
58 |
59 | $typeahead.on('typeahead:select', function(e, result) {
60 | window.location = baseURL + result.url;
61 | });
62 | });
63 |
--------------------------------------------------------------------------------
/Tests/Unit Tests/Conditions/CreditCardTypeTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CreditCardTypeTests.swift
3 | // FormValidatorSwift
4 | //
5 | // Created by Aaron McTavish on 21/12/2016.
6 | // Copyright © 2016 ustwo. All rights reserved.
7 | //
8 |
9 | import XCTest
10 |
11 | @testable import FormValidatorSwift
12 |
13 |
14 | final class CreditCardTypeTests: XCTestCase {
15 |
16 |
17 | // MARK: - Configuration
18 |
19 | private struct TestableCreditCardType {
20 | let cardType: CreditCardType
21 | let expectedDescription: String
22 | let identifier: String
23 | }
24 |
25 | private let testCases: [TestableCreditCardType] = [
26 | TestableCreditCardType(cardType: .all,
27 | expectedDescription: "American Express, Diners Club, Discover, JCB, Maestro, Master Card, VISA",
28 | identifier: "All"),
29 | TestableCreditCardType(cardType: [.americanExpress, .visa],
30 | expectedDescription: "American Express, VISA",
31 | identifier: "Some"),
32 | TestableCreditCardType(cardType: [],
33 | expectedDescription: "",
34 | identifier: "None")
35 | ]
36 |
37 |
38 | // MARK: - Test Properties
39 |
40 | func testCreditCardType_Description() {
41 | for testCase in testCases {
42 | // When
43 | let actualResult = testCase.cardType.description
44 |
45 | // Test
46 | XCTAssertEqual(testCase.expectedDescription,
47 | actualResult,
48 | "For case: \(testCase.identifier) expected the description to be: \(testCase.expectedDescription) but found: \(actualResult)")
49 | }
50 | }
51 |
52 | }
53 |
--------------------------------------------------------------------------------
/docs/docsets/FormValidatorSwift.docset/Contents/Resources/Documents/js/jazzy.search.js:
--------------------------------------------------------------------------------
1 | $(function(){
2 | var searchIndex = lunr(function() {
3 | this.ref('url');
4 | this.field('name');
5 | });
6 |
7 | var $typeahead = $('[data-typeahead]');
8 | var $form = $typeahead.parents('form');
9 | var searchURL = $form.attr('action');
10 |
11 | function displayTemplate(result) {
12 | return result.name;
13 | }
14 |
15 | function suggestionTemplate(result) {
16 | var t = '';
17 | t += '' + result.name + '';
18 | if (result.parent_name) {
19 | t += '' + result.parent_name + '';
20 | }
21 | t += '
';
22 | return t;
23 | }
24 |
25 | $typeahead.one('focus', function() {
26 | $form.addClass('loading');
27 |
28 | $.getJSON(searchURL).then(function(searchData) {
29 | $.each(searchData, function (url, doc) {
30 | searchIndex.add({url: url, name: doc.name});
31 | });
32 |
33 | $typeahead.typeahead(
34 | {
35 | highlight: true,
36 | minLength: 3
37 | },
38 | {
39 | limit: 10,
40 | display: displayTemplate,
41 | templates: { suggestion: suggestionTemplate },
42 | source: function(query, sync) {
43 | var results = searchIndex.search(query).map(function(result) {
44 | var doc = searchData[result.ref];
45 | doc.url = result.ref;
46 | return doc;
47 | });
48 | sync(results);
49 | }
50 | }
51 | );
52 | $form.removeClass('loading');
53 | $typeahead.trigger('focus');
54 | });
55 | });
56 |
57 | var baseURL = searchURL.slice(0, -"search.json".length);
58 |
59 | $typeahead.on('typeahead:select', function(e, result) {
60 | window.location = baseURL + result.url;
61 | });
62 | });
63 |
--------------------------------------------------------------------------------
/Example/macOS/FormViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FormViewController.swift
3 | // FormValidatorSwift
4 | //
5 | // Created by Aaron McTavish on 06/01/2017.
6 | // Copyright © 2017 ustwo Fampany Ltd. All rights reserved.
7 | //
8 |
9 | import AppKit
10 |
11 |
12 | import FormValidatorSwift
13 |
14 |
15 | final class FormViewController: NSViewController {
16 |
17 |
18 | // MARK: - Properties
19 |
20 | var form = ControlForm()
21 |
22 | fileprivate var underlyingView: FormView {
23 | if let myView = view as? FormView {
24 | return myView
25 | }
26 |
27 | let newView = FormView()
28 | view = newView
29 | return newView
30 | }
31 |
32 |
33 | // MARK: - View Lifecycle
34 |
35 | override func loadView() {
36 | view = FormView()
37 | }
38 |
39 | override func viewDidLoad() {
40 | super.viewDidLoad()
41 |
42 | form.addEntry(underlyingView.titleEntry.textField)
43 | form.addEntry(underlyingView.nameEntry.textField)
44 | form.addEntry(underlyingView.emailEntry.textField)
45 |
46 | underlyingView.submitButton.action = #selector(FormViewController.submitButtonPressed(_:))
47 | }
48 |
49 |
50 | // MARK: - Control Actions
51 |
52 | @objc func submitButtonPressed(_ sender: NSButton) {
53 | let alertMessage: String
54 | if form.isValid {
55 | alertMessage = NSLocalizedString("Success: Your data has been submitted!", comment: "")
56 | } else {
57 | alertMessage = NSLocalizedString("Error: Please correct your entries in the form.", comment: "")
58 | }
59 |
60 | let alert = NSAlert()
61 | alert.alertStyle = .critical
62 | alert.messageText = alertMessage
63 |
64 | alert.beginSheetModal(for: NSApplication.shared.mainWindow!, completionHandler: nil)
65 | }
66 |
67 | }
68 |
--------------------------------------------------------------------------------
/Tests/Unit Tests/Validators/CreditCardValidatorTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CreditCardValidatorTests.swift
3 | // FormValidatorSwift
4 | //
5 | // Created by Onur Ersel on 2016-11-02.
6 | // Copyright © 2016 ustwo. All rights reserved.
7 | //
8 |
9 | import XCTest
10 |
11 | @testable import FormValidatorSwift
12 |
13 |
14 | final class CreditCardValidatorTests: XCTestCase {
15 |
16 |
17 | // MARK: - Properties
18 |
19 | let validator = CreditCardValidator()
20 |
21 |
22 | // MARK: - Test Success
23 |
24 | func testCreditCardValidator_Success() {
25 | // Given
26 | let testInput = "376031710126369"
27 | let expectedResult: [Condition]? = nil
28 |
29 | // Test
30 | AssertValidator(validator, testInput: testInput, expectedResult: expectedResult)
31 | }
32 |
33 | func testCreditCardValidator_GetCardType_Success() {
34 | // Given
35 | let testInput = "5300000000000000"
36 | let expectedResult: CreditCardType = [.mastercard]
37 |
38 | let exp = self.expectation(description: "Card types should be valid")
39 |
40 | validator.validCardTypes(for: testInput) { (validCardTypes) in
41 | XCTAssertEqual(validCardTypes, expectedResult, "Card type of \(testInput) should be \(expectedResult), but returned \(validCardTypes).")
42 | exp.fulfill()
43 | }
44 |
45 | self.waitForExpectations(timeout: 3, handler: nil)
46 | }
47 |
48 |
49 | // MARK: - Test Failure
50 |
51 | func testCreditCardValidator_Failure() {
52 | // Given
53 | let testInput = "3760 a317"
54 | let expectedResult: [Condition]? = validator.conditions
55 |
56 | // Test
57 | AssertValidator(validator, testInput: testInput, expectedResult: expectedResult)
58 | }
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/Sources/Conditions/CreditCardCondition.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CreditCardCondition.swift
3 | // FormValidatorSwift
4 | //
5 | // Created by Onur Ersel on 02/11/2016.
6 | // Copyright © 2016 ustwo Fampany Ltd. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 |
12 | /**
13 | * The `CreditCardCondition` checks a string for a credit card number.
14 | */
15 | public struct CreditCardCondition: ConfigurableCondition {
16 |
17 | // MARK: - Properties
18 |
19 | public var localizedViolationString = StringLocalization.sharedInstance.localizedString("US2KeyConditionViolationCreditCard", comment: "")
20 |
21 | public var regex: String {
22 | return configuration.cardType.regex
23 | }
24 |
25 | public var shouldAllowViolation = true
26 |
27 | public var configuration: CreditCardConfiguration
28 |
29 |
30 | // MARK: - Initializers
31 |
32 | public init(configuration: CreditCardConfiguration) {
33 | self.configuration = configuration
34 | }
35 |
36 |
37 | // MARK: - Check
38 |
39 | /**
40 | Checks if the string is a valid credit card number, after removes all whitespace.
41 | */
42 | public func check(_ text: String?) -> Bool {
43 | guard let sourceText = text,
44 | let regExp = try? NSRegularExpression(pattern: self.regex, options: .caseInsensitive) else {
45 | return false
46 | }
47 |
48 | let sourceTextNs = sourceText as NSString
49 | let trimmedText = sourceTextNs.replacingOccurrences(of: "\\D", with: "", options: .regularExpression, range: NSRange(location: 0, length: sourceTextNs.length)) as String
50 |
51 | return check(trimmedText, withRegex: regExp)
52 | }
53 |
54 | public func check(_ trimmedText: String, withRegex regExp: NSRegularExpression) -> Bool {
55 | return regExp.firstMatch(in: trimmedText, options: [], range: NSRange(location: 0, length: trimmedText.count)) != nil
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Tests/Unit Tests/Conditions/URLConditionTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URLConditionTests.swift
3 | // FormValidatorSwift
4 | //
5 | // Created by Aaron McTavish on 13/01/2016.
6 | // Copyright © 2016 ustwo. All rights reserved.
7 | //
8 |
9 | import XCTest
10 |
11 | @testable import FormValidatorSwift
12 |
13 |
14 | final class URLConditionTests: XCTestCase {
15 |
16 | // MARK: - Properties
17 |
18 | let condition = URLCondition()
19 |
20 |
21 | // MARK: - Test Success
22 |
23 | func testURLCondition_Success() {
24 | // Given
25 | let testInput = "http://www.example.com/?id=12345¶m=value"
26 | let expectedResult = true
27 |
28 | // Test
29 | AssertCondition(condition, testInput: testInput, expectedResult: expectedResult)
30 | }
31 |
32 |
33 | // MARK: - Test Failure
34 |
35 | func testURLCondition_NoDomain_Failure() {
36 | // Given
37 | let testInput = "http://example"
38 | let expectedResult = false
39 |
40 | // Test
41 | AssertCondition(condition, testInput: testInput, expectedResult: expectedResult)
42 | }
43 |
44 | func testURLCondition_NoScheme_Failure() {
45 | // Given
46 | let testInput = "www.example.com"
47 | let expectedResult = false
48 |
49 | // Test
50 | AssertCondition(condition, testInput: testInput, expectedResult: expectedResult)
51 | }
52 |
53 | func testURLCondition_NonHTTPScheme_Failure() {
54 | // Given
55 | let testInput = "ftp://www.example.com"
56 | let expectedResult = false
57 |
58 | // Test
59 | AssertCondition(condition, testInput: testInput, expectedResult: expectedResult)
60 | }
61 |
62 | func testURLCondition_Nil_Failure() {
63 | // Given
64 | let testInput: String? = nil
65 | let expectedResult = false
66 |
67 | // Test
68 | AssertCondition(condition, testInput: testInput, expectedResult: expectedResult)
69 | }
70 |
71 | }
72 |
--------------------------------------------------------------------------------
/Sources/Validators/CreditCardValidator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CreditCardValidator.swift
3 | // FormValidatorSwift
4 | //
5 | // Created by Onur Ersel on 02/11/2016.
6 | // Copyright © 2016 ustwo Fampany Ltd. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /**
12 | * The `CreditCardValidator` contains an `CreditCardCondition`. A valid string is a credit card number.
13 | * - seealso: `CreditCardCondition`
14 | */
15 | public struct CreditCardValidator: ConfigurableValidator {
16 |
17 |
18 | // MARK: - Properties
19 |
20 | public var conditions: [Condition]
21 |
22 |
23 | // MARK: - Initializers
24 |
25 | public init(configuration: CreditCardConfiguration) {
26 | conditions = [CreditCardCondition(configuration: configuration)]
27 | }
28 |
29 |
30 | // MARK: - Validity
31 |
32 | /**
33 | Returns valid card types for a credit card number asynchronously.
34 |
35 | - Parameters:
36 | - creditCardNumber: Credit card number.
37 | - completion: Completion callback, returns valid card types for given number.
38 | */
39 | public func validCardTypes(`for` creditCardNumber: String, completion : @escaping (CreditCardType) -> Void) {
40 |
41 | DispatchQueue.global().async {
42 |
43 | var validCardTypes: CreditCardType = []
44 | defer {
45 | DispatchQueue.main.async {
46 | completion(validCardTypes)
47 | }
48 | }
49 |
50 | let creditCardCondition = CreditCardCondition()
51 | let trimmedCardNumber = String(creditCardNumber.filter { $0 != " " })
52 |
53 | for cardType in CreditCardType.allArray {
54 | if let regex = try? NSRegularExpression(pattern: cardType.regex, options: .caseInsensitive),
55 | creditCardCondition.check(trimmedCardNumber, withRegex: regex) {
56 | validCardTypes.insert(cardType)
57 | }
58 | }
59 |
60 | }
61 |
62 |
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/Sources/Configurations/PostcodeConfiguration.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PostcodeConfiguration.swift
3 | // FormValidatorSwift
4 | //
5 | // Created by Aaron McTavish on 04/01/2017.
6 | // Copyright © 2017 ustwo Fampany Ltd. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 |
12 | /// Countries that are supported by `PostcodeCondition`. Each postcode is an ISO 3166-1 alpha-3 country code. There is a `regex` property that returns the regex for validating that country's postcode.
13 | public enum PostcodeCountries: String {
14 |
15 | case sweden = "SWE"
16 | case turkey = "TUR"
17 | case unitedKingdom = "GBR"
18 | case unitedStates = "USA"
19 |
20 | /// The regex for validating the country's postcode.
21 | public var regex: String {
22 | switch self {
23 | case .sweden:
24 | return "^(SE-)?[0-9]{3}\\s?[0-9]{2}$"
25 | case .turkey:
26 | return "^[0-9]{5}$"
27 | case .unitedKingdom:
28 | return "^([A-PR-UWYZa-pr-uwyz]([0-9]{1,2}|([A-HK-Ya-hk-y][0-9]|[A-HK-Ya-hk-y][0-9]([0-9]|[ABEHMNPRV-Yabehmnprv-y]))|[0-9][A-HJKS-UWa-hjks-uw])\\ {0,1}[0-9][ABD-HJLNP-UW-Zabd-hjlnp-uw-z]{2}|([Gg][Ii][Rr]\\ 0[Aa][Aa])|([Ss][Aa][Nn]\\ {0,1}[Tt][Aa]1)|([Bb][Ff][Pp][Oo]\\ {0,1}([Cc]\\/[Oo]\\ )?[0-9]{1,4})|(([Aa][Ss][Cc][Nn]|[Bb][Bb][Nn][Dd]|[BFSbfs][Ii][Qq][Qq]|[Pp][Cc][Rr][Nn]|[Ss][Tt][Hh][Ll]|[Tt][Dd][Cc][Uu]|[Tt][Kk][Cc][Aa])\\ {0,1}1[Zz][Zz]))$"
29 | case .unitedStates:
30 | return "^[0-9]{5}(?:[-|\\s][0-9]{4})?$"
31 | }
32 | }
33 |
34 | public static let allValues: [PostcodeCountries] = [.sweden, .turkey, .unitedKingdom, .unitedStates]
35 | }
36 |
37 |
38 | /// Stores configuration for `PostcodeCondition`.
39 | public struct PostcodeConfiguration: Configuration {
40 |
41 |
42 | // MARK: - Properties
43 |
44 | public var country: PostcodeCountries
45 |
46 |
47 | // MARK: - Initializers
48 |
49 | public init() {
50 | self.init(country: .unitedKingdom)
51 | }
52 |
53 | public init(country: PostcodeCountries = .unitedKingdom) {
54 | self.country = country
55 | }
56 |
57 | }
58 |
--------------------------------------------------------------------------------
/Tests/Unit Tests/Validators/CompositeValidatorTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CompositeValidatorTests.swift
3 | // FormValidatorSwift
4 | //
5 | // Created by Aaron McTavish on 14/01/2016.
6 | // Copyright © 2016 ustwo. All rights reserved.
7 | //
8 |
9 | import XCTest
10 |
11 | @testable import FormValidatorSwift
12 |
13 |
14 | final class CompositeValidatorTests: XCTestCase {
15 |
16 |
17 | // MARK: - Properties
18 |
19 | let firstValidator = RangeValidator(configuration: ConfigurationSeeds.RangeSeeds.zeroToFour)
20 | let secondValidator = AlphanumericValidator()
21 |
22 |
23 | // MARK: - Test Success
24 |
25 | func testCompositeValidator_Success() {
26 | // Given
27 | let testInput = "1A23"
28 | let validator = CompositeValidator(validators: [firstValidator, secondValidator])
29 | let expectedResult: [Condition]? = nil
30 |
31 | // When
32 | let actualResult = validator.checkConditions(testInput)
33 |
34 | // Initial Tests
35 | XCTAssertNil(firstValidator.checkConditions(testInput))
36 | XCTAssertNil(secondValidator.checkConditions(testInput))
37 |
38 | // Test
39 | XCTAssertNil(actualResult, "The `\(type(of: validator))` should respond with \(expectedResult.debugDescription) and but received \(actualResult.debugDescription).")
40 | }
41 |
42 |
43 | // MARK: - Test Failure
44 |
45 | func testCompositeValidator_Failure() {
46 | // Given
47 | let testInput = "1A234"
48 | let validator = CompositeValidator(validators: [firstValidator, secondValidator])
49 | let expectedResult: [Condition]? = nil
50 |
51 | // When
52 | let actualResult = validator.checkConditions(testInput)
53 |
54 | // Initial Tests
55 | XCTAssertNotNil(firstValidator.checkConditions(testInput))
56 | XCTAssertNil(secondValidator.checkConditions(testInput))
57 |
58 | // Test
59 | XCTAssertNotNil(actualResult, "The `\(type(of: validator))` should respond with \(expectedResult.debugDescription) and but received \(actualResult.debugDescription).")
60 | }
61 |
62 | }
63 |
--------------------------------------------------------------------------------
/Tests/Unit Tests/Conditions/RangeConditionTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RangeConditionTests.swift
3 | // FormValidatorSwift
4 | //
5 | // Created by Aaron McTavish on 13/01/2016.
6 | // Copyright © 2016 ustwo. All rights reserved.
7 | //
8 |
9 | import XCTest
10 |
11 | @testable import FormValidatorSwift
12 |
13 |
14 | final class RangeConditionTests: XCTestCase {
15 |
16 |
17 | // MARK: - Properties
18 |
19 | let condition = RangeCondition(configuration: ConfigurationSeeds.RangeSeeds.threeToThirteen)
20 |
21 |
22 | // MARK: - Test Initializers
23 |
24 | func testRangeCondition_DefaultInit() {
25 | // Given
26 | let condition = RangeCondition()
27 | let expectedRange = 0..<1
28 |
29 | // When
30 | let actualRange = condition.configuration.range
31 |
32 | // Test
33 | XCTAssertEqual(actualRange,
34 | expectedRange,
35 | "Expected range to be: \(expectedRange) but found: \(actualRange)")
36 | }
37 |
38 |
39 | // MARK: - Test Success
40 |
41 | func testRangeCondition_Success() {
42 | // Given
43 | let testInput = "1A2B3D4C5D"
44 | let expectedResult = true
45 |
46 | // Test
47 | AssertCondition(condition, testInput: testInput, expectedResult: expectedResult)
48 | }
49 |
50 |
51 | // MARK: - Test Failure
52 |
53 | func testRangeCondition_Start_Failure() {
54 | // Given
55 | let testInput = "1A"
56 | let expectedResult = false
57 |
58 | // Test
59 | AssertCondition(condition, testInput: testInput, expectedResult: expectedResult)
60 | }
61 |
62 | func testRangeCondition_Length_Failure() {
63 | // Given
64 | let testInput = "1A2B3D4C5D6E"
65 | let expectedResult = false
66 |
67 | // Test
68 | AssertCondition(condition, testInput: testInput, expectedResult: expectedResult)
69 | }
70 |
71 | func testRangeCondition_Nil_Failure() {
72 | // Given
73 | let testInput: String? = nil
74 | let expectedResult = false
75 |
76 | // Test
77 | AssertCondition(condition, testInput: testInput, expectedResult: expectedResult)
78 | }
79 |
80 | }
81 |
--------------------------------------------------------------------------------
/Example/iOS/FormViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // FormValidatorSwift
4 | //
5 | // Created by Aaron McTavish on 14/01/2016.
6 | // Copyright © 2016 ustwo Fampany Ltd. All rights reserved.
7 | //
8 |
9 | import FormValidatorSwift
10 | import UIKit
11 |
12 |
13 | final class FormViewController: UIViewController {
14 |
15 |
16 | // MARK: - Properties
17 |
18 | var form = ControlForm()
19 |
20 | fileprivate var underlyingView: FormView {
21 | if let myView = view as? FormView {
22 | return myView
23 | }
24 |
25 | let newView = FormView()
26 | view = newView
27 | return newView
28 | }
29 |
30 |
31 | // MARK: - View Lifecycle
32 |
33 | override func loadView() {
34 | view = FormView()
35 | }
36 |
37 | override func viewDidLoad() {
38 | super.viewDidLoad()
39 |
40 | form.addEntry(underlyingView.titleEntry.textField)
41 | form.addEntry(underlyingView.nameEntry.textField)
42 | form.addEntry(underlyingView.emailEntry.textField)
43 |
44 | underlyingView.submitButton.addTarget(self, action: #selector(FormViewController.submitButtonPressed(_:)), for: .touchUpInside)
45 | }
46 |
47 |
48 | // MARK: - Control Actions
49 |
50 | @objc func submitButtonPressed(_ sender: UIButton) {
51 | let alertTitle: String
52 | let alertMessage: String
53 | if form.isValid {
54 | alertTitle = NSLocalizedString("Success", comment: "")
55 | alertMessage = NSLocalizedString("Your data has been submitted!", comment: "")
56 | } else {
57 | alertTitle = NSLocalizedString("Error", comment: "")
58 | alertMessage = NSLocalizedString("Please correct your entries in the form.", comment: "")
59 | }
60 |
61 | let alertController = UIAlertController(title: alertTitle, message: alertMessage, preferredStyle: .alert)
62 | alertController.popoverPresentationController?.sourceView = sender
63 |
64 | let doneAction = UIAlertAction(title: NSLocalizedString("Done", comment: ""), style: .default, handler: nil)
65 | alertController.addAction(doneAction)
66 |
67 | present(alertController, animated: true, completion: nil)
68 | }
69 |
70 | }
71 |
--------------------------------------------------------------------------------
/Tests/Unit Tests/Conditions/EmailConditionTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EmailConditionTests.swift
3 | // FormValidatorSwift
4 | //
5 | // Created by Aaron McTavish on 13/01/2016.
6 | // Copyright © 2016 ustwo. All rights reserved.
7 | //
8 |
9 | import XCTest
10 |
11 | @testable import FormValidatorSwift
12 |
13 |
14 | final class EmailConditionTests: XCTestCase {
15 |
16 |
17 | // MARK: - Properties
18 |
19 | let condition = EmailCondition()
20 |
21 |
22 | // MARK: - Test Success
23 |
24 | func testEmailCondition_Success() {
25 | // Given
26 | let testInput = "e_x.a+m-p_l.e@ex.example-example.ex.am"
27 | let expectedResult = true
28 |
29 | // Test
30 | AssertCondition(condition, testInput: testInput, expectedResult: expectedResult)
31 | }
32 |
33 |
34 | // MARK: - Test Failure
35 |
36 | func testEmailCondition_NoAt_Failure() {
37 | // Given
38 | let testInput = "example"
39 | let expectedResult = false
40 |
41 | // Test
42 | AssertCondition(condition, testInput: testInput, expectedResult: expectedResult)
43 | }
44 |
45 | func testEmailCondition_NoDomain_Failure() {
46 | // Given
47 | let testInput = "example@"
48 | let expectedResult = false
49 |
50 | // Test
51 | AssertCondition(condition, testInput: testInput, expectedResult: expectedResult)
52 | }
53 |
54 | func testEmailCondition_PartialDomain_Failure() {
55 | // Given
56 | let testInput = "example@example.ex."
57 | let expectedResult = false
58 |
59 | // Test
60 | AssertCondition(condition, testInput: testInput, expectedResult: expectedResult)
61 | }
62 |
63 | func testEmailCondition_Space_Failure() {
64 | // Given
65 | let testInput = "e xample@example.ex."
66 | let expectedResult = false
67 |
68 | // Test
69 | AssertCondition(condition, testInput: testInput, expectedResult: expectedResult)
70 | }
71 |
72 | func testEmailCondition_ReservedCharacters_Failure() {
73 | // Given
74 | let testInput = "e/xample@example.ex."
75 | let expectedResult = false
76 |
77 | // Test
78 | AssertCondition(condition, testInput: testInput, expectedResult: expectedResult)
79 | }
80 |
81 | }
82 |
--------------------------------------------------------------------------------
/Tests/Unit Tests/Conditions/URLShorthandConditionTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URLShorthandConditionTests.swift
3 | // FormValidatorSwift
4 | //
5 | // Created by Aaron McTavish on 13/01/2016.
6 | // Copyright © 2016 ustwo. All rights reserved.
7 | //
8 |
9 | import XCTest
10 |
11 | @testable import FormValidatorSwift
12 |
13 |
14 | final class URLShorthandConditionTests: XCTestCase {
15 |
16 |
17 | // MARK: - Properties
18 |
19 | let condition = URLShorthandCondition()
20 |
21 |
22 | // MARK: - Test Success
23 |
24 | func testURLShorthandCondition_FullURL_Success() {
25 | // Given
26 | let testInput = "http://www.example.com/?id=12345¶m=value"
27 | let expectedResult = true
28 |
29 | // Test
30 | AssertCondition(condition, testInput: testInput, expectedResult: expectedResult)
31 | }
32 |
33 | func testURLShorthandCondition_ShortURL_Success() {
34 | // Given
35 | let testInput = "example.com"
36 | let expectedResult = true
37 |
38 | // Test
39 | AssertCondition(condition, testInput: testInput, expectedResult: expectedResult)
40 | }
41 |
42 |
43 | // MARK: - Test Failure
44 |
45 | func testURLShorthandCondition_NoDomain_Failure() {
46 | // Given
47 | let testInput = "http://example"
48 | let expectedResult = false
49 |
50 | // Test
51 | AssertCondition(condition, testInput: testInput, expectedResult: expectedResult)
52 | }
53 |
54 | func testURLShorthandCondition_NoPath_Failure() {
55 | // Given
56 | let testInput = "http://"
57 | let expectedResult = false
58 |
59 | // Test
60 | AssertCondition(condition, testInput: testInput, expectedResult: expectedResult)
61 | }
62 |
63 | func testURLShorthandCondition_NonHTTPScheme_Failure() {
64 | // Given
65 | let testInput = "ftp://www.example.com"
66 | let expectedResult = false
67 |
68 | // Test
69 | AssertCondition(condition, testInput: testInput, expectedResult: expectedResult)
70 | }
71 |
72 | func testURLShorthandCondition_Nil_Failure() {
73 | // Given
74 | let testInput: String? = nil
75 | let expectedResult = false
76 |
77 | // Test
78 | AssertCondition(condition, testInput: testInput, expectedResult: expectedResult)
79 | }
80 |
81 | }
82 |
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | disabled_rules:
2 | - line_length
3 | - nesting
4 | opt_in_rules:
5 | - anyobject_protocol
6 | - array_init
7 | - attributes
8 | - closure_end_indentation
9 | - closure_spacing
10 | - conditional_returns_on_newline
11 | - contains_over_first_not_nil
12 | - convenience_type
13 | - empty_count
14 | - empty_string
15 | - explicit_init
16 | - fallthrough
17 | - fatal_error_message
18 | - file_header
19 | - first_where
20 | - implicit_return
21 | - implicitly_unwrapped_optional
22 | - number_separator
23 | - object_literal
24 | - overridden_super_call
25 | - private_outlet
26 | - prohibited_super_call
27 | - redundant_nil_coalescing
28 | - sorted_imports
29 | - switch_case_on_newline
30 | - unneeded_parentheses_in_closure_argument
31 | - yoda_condition
32 |
33 | excluded:
34 | - Pods
35 | - Tests
36 | - vendor
37 |
38 | attributes:
39 | always_on_same_line:
40 | - '@IBAction'
41 | - '@NSManaged'
42 | - '@nonobjc'
43 | - '@objc'
44 |
45 | file_header:
46 | required_pattern: |
47 | \/\/
48 | \/\/ .*?\.swift
49 | \/\/ FormValidatorSwift
50 | \/\/
51 | \/\/ Created by .*? on \d{1,2}\/\d{1,2}\/\d{4}\.
52 | \/\/ Copyright © \d{4} ustwo Fampany Ltd\. All rights reserved\.
53 | \/\/
54 |
55 | line_length: 110
56 | trailing_whitespace:
57 | ignores_empty_lines: true
58 | ignores_comments: true
59 | type_body_length:
60 | - 300 # warning
61 | - 400 # error
62 | vertical_whitespace:
63 | max_empty_lines: 2
64 |
65 | custom_rules:
66 | # pragma mark style
67 | marks_empty_space:
68 | name: "Marks"
69 | regex: "(//MARK)"
70 | message: "There should be an empty space between // and the MARK."
71 | severity: warning
72 |
73 | marks_style:
74 | name: "Marks"
75 | regex: "(// MARK: -?[a-zA-Z0-9])"
76 | message: "Marks should follow the following structure: // MARK: - Comment."
77 | severity: warning
78 |
79 | # comments style
80 | comments_empty_space:
81 | name: "Comments"
82 | regex: "(//[a-zA-Z0-9])"
83 | match_kinds:
84 | - comment
85 | message: "There should be an empty space between // and the comment."
86 | severity: warning
87 |
88 | comments_empty_line_after:
89 | name: "Comments"
90 | regex: "([^\n]\n^ *[a-zA-Z0-9{])"
91 | match_kinds:
92 | - comment
93 | message: "There should be an empty line after a comment."
94 | severity: warning
95 |
--------------------------------------------------------------------------------
/Tests/Unit Tests/Conditions/Logic/OrConditionTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OrConditionTests.swift
3 | // FormValidatorSwift
4 | //
5 | // Created by Aaron McTavish on 13/01/2016.
6 | // Copyright © 2016 ustwo. All rights reserved.
7 | //
8 |
9 | import XCTest
10 |
11 | @testable import FormValidatorSwift
12 |
13 |
14 | final class OrConditionTests: XCTestCase {
15 |
16 | let firstCondition = RangeCondition(configuration: ConfigurationSeeds.RangeSeeds.zeroToFour)
17 | let secondCondition = AlphanumericCondition()
18 |
19 |
20 | // MARK: - Test Initializers
21 |
22 | func testOrCondition_DefaultInit() {
23 | // Given
24 | let condition = OrCondition()
25 | let expectedCount = 1
26 |
27 | // When
28 | let actualCount = condition.conditions.count
29 |
30 | // Test
31 | XCTAssertEqual(actualCount,
32 | expectedCount,
33 | "Expected number of conditions to be: \(expectedCount) but found: \(actualCount)")
34 | }
35 |
36 |
37 | // MARK: - Test Success
38 |
39 | func testOrCondition_Success() {
40 | // Given
41 | let testInput = ""
42 | var condition = OrCondition(conditions: [firstCondition, secondCondition])
43 | let expectedResult = true
44 |
45 | // When
46 | condition.localizedViolationString = "Min 0 Max 4 or must only contain alphanumeric"
47 |
48 | // Initial Tests
49 | AssertCondition(firstCondition, testInput: testInput, expectedResult: true)
50 | AssertCondition(secondCondition, testInput: testInput, expectedResult: false)
51 |
52 | // Test
53 | AssertCondition(condition, testInput: testInput, expectedResult: expectedResult)
54 | }
55 |
56 |
57 | // MARK: - Test Failure
58 |
59 | func testOrCondition_Failure() {
60 | // Given
61 | let testInput = "1A234?"
62 | var condition = OrCondition(conditions: [firstCondition, secondCondition])
63 | let expectedResult = false
64 |
65 | // When
66 | condition.localizedViolationString = "Min 0 Max 4 or must only contain alphanumeric"
67 |
68 | // Initial Tests
69 | AssertCondition(firstCondition, testInput: testInput, expectedResult: false)
70 | AssertCondition(secondCondition, testInput: testInput, expectedResult: false)
71 |
72 | // Test
73 | AssertCondition(condition, testInput: testInput, expectedResult: expectedResult)
74 | }
75 |
76 | }
77 |
--------------------------------------------------------------------------------
/Tests/Unit Tests/Configurations/ConfigurationSeeds.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ConfigurationSeeds.swift
3 | // FormValidatorSwift
4 | //
5 | // Created by Aaron McTavish on 03/01/2017.
6 | // Copyright © 2017 ustwo. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | @testable import FormValidatorSwift
12 |
13 |
14 | /// Seed data for the various configurations. These are preconfigured configurations to use during testing.
15 | struct ConfigurationSeeds {
16 |
17 | struct AlphabeticSeeds {
18 |
19 | static let noUnicode_NoWhitespace = AlphabeticConfiguration(allowsUnicode: false, allowsWhitespace: false)
20 | static let noUnicode_Whitespace = AlphabeticConfiguration(allowsUnicode: false, allowsWhitespace: true)
21 | static let unicode_NoWhitespace = AlphabeticConfiguration(allowsUnicode: true, allowsWhitespace: false)
22 | static let unicode_Whitespace = AlphabeticConfiguration(allowsUnicode: true, allowsWhitespace: true)
23 |
24 | }
25 |
26 | struct AlphanumericSeeds {
27 |
28 | static let noUnicode_NoWhitespace = AlphanumericConfiguration(allowsUnicode: false, allowsWhitespace: false)
29 | static let noUnicode_Whitespace = AlphanumericConfiguration(allowsUnicode: false, allowsWhitespace: true)
30 | static let unicode_NoWhitespace = AlphanumericConfiguration(allowsUnicode: true, allowsWhitespace: false)
31 | static let unicode_Whitespace = AlphanumericConfiguration(allowsUnicode: true, allowsWhitespace: true)
32 |
33 | }
34 |
35 | struct NumericSeeds {
36 |
37 | static let noUnicode_NoWhitespace = NumericConfiguration(allowsUnicode: false, allowsWhitespace: false)
38 | static let noUnicode_Whitespace = NumericConfiguration(allowsUnicode: false, allowsWhitespace: true)
39 | static let unicode_NoWhitespace = NumericConfiguration(allowsUnicode: true, allowsWhitespace: false)
40 | static let unicode_Whitespace = NumericConfiguration(allowsUnicode: true, allowsWhitespace: true)
41 |
42 | }
43 |
44 | struct PasswordStrengthSeeds {
45 |
46 | static let veryStrong = PasswordStrengthConfiguration(requiredStrength: .veryStrong)
47 | static let veryWeak = PasswordStrengthConfiguration(requiredStrength: .veryWeak)
48 |
49 | }
50 |
51 | struct RangeSeeds {
52 |
53 | static let threeToThirteen = RangeConfiguration(range: 3..<13)
54 | static let zeroToFour = RangeConfiguration(range: 0..<4)
55 |
56 | }
57 |
58 | }
59 |
--------------------------------------------------------------------------------
/Tests/Unit Tests/Conditions/Logic/AndConditionTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AndConditionTests.swift
3 | // FormValidatorSwift
4 | //
5 | // Created by Aaron McTavish on 13/01/2016.
6 | // Copyright © 2016 ustwo. All rights reserved.
7 | //
8 |
9 | import XCTest
10 |
11 | @testable import FormValidatorSwift
12 |
13 |
14 | final class AndConditionTests: XCTestCase {
15 |
16 |
17 | // MARK: - Properties
18 |
19 | let firstCondition = RangeCondition(configuration: ConfigurationSeeds.RangeSeeds.zeroToFour)
20 | let secondCondition = AlphanumericCondition()
21 |
22 |
23 | // MARK: - Test Initializers
24 |
25 | func testAndCondition_DefaultInit() {
26 | // Given
27 | let condition = AndCondition()
28 | let expectedCount = 1
29 |
30 | // When
31 | let actualCount = condition.conditions.count
32 |
33 | // Test
34 | XCTAssertEqual(actualCount,
35 | expectedCount,
36 | "Expected number of conditions to be: \(expectedCount) but found: \(actualCount)")
37 | }
38 |
39 |
40 | // MARK: - Test Success
41 |
42 | func testAndCondition_Success() {
43 | // Given
44 | let testInput = "1A23"
45 | var condition = AndCondition(conditions: [firstCondition, secondCondition])
46 | let expectedResult = true
47 |
48 | // When
49 | condition.localizedViolationString = "Min 0 Max 4 or must only contain alphanumeric"
50 |
51 | // Initial Tests
52 | AssertCondition(firstCondition, testInput: testInput, expectedResult: true)
53 | AssertCondition(secondCondition, testInput: testInput, expectedResult: true)
54 |
55 | // Test
56 | AssertCondition(condition, testInput: testInput, expectedResult: expectedResult)
57 | }
58 |
59 |
60 | // MARK: - Test Failure
61 |
62 | func testAndCondition_Failure() {
63 | // Given
64 | let testInput = "1A234"
65 | var condition = AndCondition(conditions: [firstCondition, secondCondition])
66 | let expectedResult = false
67 |
68 | // When
69 | condition.localizedViolationString = "Min 0 Max 4 or must only contain alphanumeric"
70 |
71 | // Initial Tests
72 | AssertCondition(firstCondition, testInput: testInput, expectedResult: false)
73 | AssertCondition(secondCondition, testInput: testInput, expectedResult: true)
74 |
75 | // Test
76 | AssertCondition(condition, testInput: testInput, expectedResult: expectedResult)
77 | }
78 |
79 | }
80 |
--------------------------------------------------------------------------------
/Sources/Conditions/PasswordStrengthCondition.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PasswordStrengthCondition.swift
3 | // FormValidatorSwift
4 | //
5 | // Created by Aaron McTavish on 13/01/2016.
6 | // Copyright © 2016 ustwo Fampany Ltd. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 |
12 | /**
13 | * The `PasswordStrengthCondition` checks for the strength of a password string.
14 | * The strength is measured on five simple criteria:
15 | * - contains lower case characters
16 | * - contains upper case characters
17 | * - contains numeric characters
18 | * - contains special characters (e.g /';~)
19 | * - is more than 8 characters long
20 | *
21 | * Each of these matched criteria moves the password strength of the string up one strength, strength is measured on `PasswordStrength`.
22 | *
23 | * If the password strength matches or is above the required strength than the condition will pass.
24 | */
25 | public struct PasswordStrengthCondition: ConfigurableCondition {
26 |
27 |
28 | // MARK: - Properties
29 |
30 | public var localizedViolationString = StringLocalization.sharedInstance.localizedString("US2KeyConditionViolationPasswordStrength", comment: "")
31 |
32 | public let regex = ""
33 |
34 | public var shouldAllowViolation = true
35 |
36 | public let configuration: PasswordStrengthConfiguration
37 |
38 |
39 | // MARK: - Initializers
40 |
41 | public init(configuration: PasswordStrengthConfiguration) {
42 | self.configuration = configuration
43 | }
44 |
45 |
46 | // MARK: - Check
47 |
48 | public func check(_ text: String?) -> Bool {
49 | guard let sourceText = text else {
50 | return false
51 | }
52 |
53 | let matches = [numberOfMatchesWithPattern("\\d", text: sourceText), numberOfMatchesWithPattern("[a-z]", text: sourceText), numberOfMatchesWithPattern("[A-Z]", text: sourceText), numberOfMatchesWithPattern("[^a-zA-Z\\d]", text: sourceText)]
54 |
55 | var strength = matches.reduce(0, { $0 + ($1 > 0 ? 1 : 0) })
56 |
57 | if sourceText.count > 8 {
58 | strength += 1
59 | } else {
60 | strength -= 1
61 | }
62 |
63 | return strength >= configuration.requiredStrength.rawValue
64 | }
65 |
66 | fileprivate func numberOfMatchesWithPattern(_ pattern: String, text: String) -> Int {
67 | guard let regExpression = try? NSRegularExpression(pattern: pattern, options: []) else {
68 | return 0
69 | }
70 |
71 | return regExpression.numberOfMatches(in: text, options: [], range: NSRange(location: 0, length: text.count))
72 | }
73 |
74 | }
75 |
--------------------------------------------------------------------------------
/Sources/Protocols/Condition.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Condition.swift
3 | // FormValidatorSwift
4 | //
5 | // Created by Aaron McTavish on 12/01/2016.
6 | // Copyright © 2016 ustwo Fampany Ltd. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 |
12 | /**
13 | * A _Condition_ is the smallest sub element of the validation framework.
14 | * It tells how a string must be structured or wat is has to contain or not.
15 | * Validators are for storing those conditions and checking
16 | * for violations of every condition.
17 | *
18 | * Conditions are recommended working with regular expressions but can also contain
19 | * their custom checking code for detecting violations in the string to check.
20 | *
21 | * By returning a localized string in method `localizedViolationString` the
22 | * user can be informed in a convenient way what went wrong.
23 | */
24 | public protocol Condition: CustomStringConvertible {
25 |
26 | /// Localized string which described the kind of violation.
27 | var localizedViolationString: String { get set }
28 |
29 | /// A regular expression string which the validated string is matched against.
30 | var regex: String { get }
31 |
32 | /// If set to `false` the user is not able to enter characters which would break the condition.
33 | var shouldAllowViolation: Bool { get set }
34 |
35 | /// Initializer that creates a condition based on default values.
36 | init()
37 |
38 | /**
39 | Check the custom condition.
40 | - parameter text: `String` to check.
41 | - returns: Whether the condition check passed or failed.
42 | - note: Checking a `nil` value should always return `false`.
43 | */
44 | func check(_ text: String?) -> Bool
45 | }
46 |
47 |
48 | // Default implementation of `Condition.check(text:)` returns `true` if the `regex` is valid and there is at least one match in `text`.
49 | public extension Condition {
50 |
51 | func check(_ text: String?) -> Bool {
52 | guard let sourceText = text,
53 | let regExpression = try? NSRegularExpression(pattern: regex, options: .caseInsensitive) else {
54 | return false
55 | }
56 |
57 | return regExpression.firstMatch(in: sourceText, options: [], range: NSRange(location: 0, length: sourceText.count)) != nil
58 | }
59 |
60 | }
61 |
62 |
63 | public extension Condition {
64 |
65 | var description: String {
66 | var result = "<"
67 |
68 | result += "\(type(of: self))"
69 | result += "\n "
70 | result += "\n "
71 |
72 | result += ">"
73 |
74 | return result
75 | }
76 |
77 | }
78 |
--------------------------------------------------------------------------------
/Tests/Unit Tests/Conditions/PasswordStrengthConditionTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PasswordStrengthConditionTests.swift
3 | // FormValidatorSwift
4 | //
5 | // Created by Aaron McTavish on 13/01/2016.
6 | // Copyright © 2016 ustwo. All rights reserved.
7 | //
8 |
9 | import XCTest
10 |
11 | @testable import FormValidatorSwift
12 |
13 |
14 | final class PasswordStrengthConditionTests: XCTestCase {
15 |
16 |
17 | // MARK: - Test Initializers
18 |
19 | func testPasswordStrengthCondition_DefaultInit() {
20 | // Given
21 | let condition = PasswordStrengthCondition()
22 | let expectedStrength = PasswordStrength.veryStrong
23 |
24 | // When
25 | let actualStrength = condition.configuration.requiredStrength
26 |
27 | // Test
28 | XCTAssertEqual(actualStrength,
29 | expectedStrength,
30 | "Expected required strength to be: \(expectedStrength) but found: \(actualStrength)")
31 | }
32 |
33 |
34 | // MARK: - Test Success
35 |
36 | func testPasswordStrengthCondition_VeryWeak_Success() {
37 | // Given
38 | let testInput = "Foo"
39 | let condition = PasswordStrengthCondition(configuration: ConfigurationSeeds.PasswordStrengthSeeds.veryWeak)
40 | let expectedResult = true
41 |
42 | // Test
43 | AssertCondition(condition, testInput: testInput, expectedResult: expectedResult)
44 | }
45 |
46 | func testPasswordStrengthCondition_VeryStrong_Success() {
47 | // Given
48 | let testInput = "F1@b9a_c12983y"
49 | let condition = PasswordStrengthCondition(configuration: ConfigurationSeeds.PasswordStrengthSeeds.veryStrong)
50 | let expectedResult = true
51 |
52 | // Test
53 | AssertCondition(condition, testInput: testInput, expectedResult: expectedResult)
54 | }
55 |
56 |
57 | // MARK: - Test Failure
58 |
59 | func testPasswordStrengthCondition_VeryStrong_Failure() {
60 | // Given
61 | let testInput = "Foo"
62 | let condition = PasswordStrengthCondition(configuration: ConfigurationSeeds.PasswordStrengthSeeds.veryStrong)
63 | let expectedResult = false
64 |
65 | // Test
66 | AssertCondition(condition, testInput: testInput, expectedResult: expectedResult)
67 | }
68 |
69 | func testPasswordStrengthCondition_Nil_Failure() {
70 | // Given
71 | let testInput: String? = nil
72 | let condition = PasswordStrengthCondition()
73 | let expectedResult = false
74 |
75 | // Test
76 | AssertCondition(condition, testInput: testInput, expectedResult: expectedResult)
77 | }
78 |
79 | }
80 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | First, thanks for taking the time to submit a pull request! These are the few notes and guidelines to keep things coherent.
4 |
5 | ## Overview
6 |
7 | 1. Read and abide by the [Code of Conduct][code-of-conduct].
8 | 1. [Fork the project](https://github.com/ustwo/formvalidator-swift/fork) and clone.
9 | 1. Check you have all [requirements](#requirements) in place.
10 | 1. Create your [_feature_ branch](#feature-branch).
11 | 1. [Install](#install) the project dependencies for development.
12 | 1. [Test](#test).
13 | 1. If you have more than trivial changes (e.g. fixing typo's), then you must include a description in the [CHANGELOG.md][changelog].
14 | 1. Push your branch and submit a [Pull Request](https://github.com/ustwo/formvalidator-swift/compare/).
15 |
16 | We will review the changes as soon as possible.
17 |
18 | ## Requirements
19 |
20 | - [Xcode][xcode]
21 | - [Bundler][bundler]
22 | - [SwiftLint][swiftlint]
23 |
24 | ## Feature Branch
25 |
26 | Please use a descriptive and concise name for your feature branch. Below is a snippet to show how to create a branch with git.
27 |
28 | ```sh
29 | git checkout -b feature/feature-name
30 | ```
31 |
32 | ## Install
33 |
34 | After installing [Xcode][xcode], [Bundler][bundler], and [SwiftLint][swiftlint] run the following terminal command from the project folder to install the remaining gems to be able to run all the tests and scripts as you would on the CI:
35 |
36 | ```sh
37 | bundle install
38 | ```
39 |
40 | ## Test
41 |
42 | You can quickly run the test suite using [Fastlane][fastlane]. From the project directly, use the following terminal command:
43 |
44 | ```sh
45 | bundle exec fastlane test
46 | ```
47 |
48 | Alternatively, you can run individual tests or the suite from [within Xcode][xcode-tests].
49 |
50 | ## Release
51 |
52 | To bump the version numbers, create the git tag, and create a GitHub release, run (replacing `$NEW_VERSION` with the desired new version number):
53 |
54 | ```sh
55 | bundle exec fastlane bump_version version:$NEW_VERSION
56 | ```
57 |
58 | Then update the [latest release][latest-release] with notes on GitHub.
59 |
60 | Lastly, [submit the updated `podspec`][cocoapods-submission] to CocoaPods for release.
61 |
62 |
63 |
64 | [bundler]: http://bundler.io/
65 | [changelog]: ../CHANGELOG.md
66 | [cocoapods-submission]: https://guides.cocoapods.org/making/making-a-cocoapod.html#release
67 | [code-of-conduct]: ../CODE_OF_CONDUCT.md
68 | [fastlane]: https://fastlane.tools/
69 | [latest-release]: https://github.com/ustwo/formvalidator-swift/releases
70 | [swiftlint]: https://github.com/realm/SwiftLint
71 | [xcode]: https://itunes.apple.com/gb/app/xcode/id497799835?mt=12#
72 | [xcode-tests]: https://developer.apple.com/library/content/documentation/DeveloperTools/Conceptual/testing_with_xcode/chapters/05-running_tests.html
73 |
--------------------------------------------------------------------------------
/Tests/Unit Tests/Extensions/XCTestCase+Additions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // XCTestCase+Additions.swift
3 | // FormValidatorSwift
4 | //
5 | // Created by Aaron McTavish on 13/01/2016.
6 | // Copyright © 2016 ustwo. All rights reserved.
7 | //
8 |
9 | import XCTest
10 |
11 |
12 | @testable import FormValidatorSwift
13 |
14 |
15 | extension XCTestCase {
16 |
17 | func AssertCondition(_ condition: Condition, testInput: String?, expectedResult: Bool, file: String = #file, line: UInt = #line) {
18 | // When
19 | let actualResult = condition.check(testInput)
20 |
21 | // Test
22 | if expectedResult != actualResult {
23 | let message = "The `\(type(of: condition))` should respond with \(expectedResult) but received \(actualResult)."
24 |
25 | #if os(iOS) || os(macOS)
26 | self.recordFailure(withDescription: message, inFile: file, atLine: Int(line), expected: true)
27 | #elseif os(tvOS)
28 | self.recordFailure(withDescription: message, inFile: file, atLine: Int(line), expected: true)
29 | #endif
30 | }
31 | }
32 |
33 | /// Asserts that a `Validator` with a given input returns the expected result.
34 | ///
35 | /// - Parameters:
36 | /// - validator: `Validator` to test.
37 | /// - testInput: Input to have `validator` check.
38 | /// - expectedResult: The expected result for running the validator's check.
39 | /// - file: File that the assertion is being run in. Defaults to `#file`.
40 | /// - line: Line that the assertion is being run from. Defaults to `#line`.
41 | ///
42 | /// - Note: Currently this checks that either the actual result and expected result are either both nil or both non-nil and have the same number of elements. As `Condition` currently does not conform to equatable, further checks are not done to ensure that the conditions in the array are the same.
43 | func AssertValidator(_ validator: Validator, testInput: String?, expectedResult: [Condition]?, file: String = #file, line: UInt = #line) {
44 | // When
45 | let actualResult = validator.checkConditions(testInput)
46 |
47 | // Test
48 | guard actualResult != nil || expectedResult != nil else {
49 | return
50 | }
51 |
52 | if let actualResult = actualResult,
53 | let expectedResult = expectedResult,
54 | actualResult.count == expectedResult.count {
55 |
56 | return
57 | }
58 |
59 | let message = "The `\(type(of: validator))` should respond with \(expectedResult.debugDescription) and but received \(actualResult.debugDescription)."
60 |
61 | #if os(iOS) || os(macOS)
62 | self.recordFailure(withDescription: message, inFile: file, atLine: Int(line), expected: true)
63 | #elseif os(tvOS)
64 | self.recordFailure(withDescription: message, inFile: file, atLine: Int(line), expected: true)
65 | #endif
66 | }
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, gender identity and expression, level of experience,
9 | nationality, personal appearance, race, religion, or sexual identity and
10 | orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at [INSERT EMAIL ADDRESS]. The project team
59 | will review and investigate all complaints, and will respond in a way that it deems
60 | appropriate to the circumstances. The project team is obligated to maintain
61 | confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at [http://contributor-covenant.org/version/1/4][version]
72 |
73 | [homepage]: http://contributor-covenant.org
74 | [version]: http://contributor-covenant.org/version/1/4/
75 |
--------------------------------------------------------------------------------
/Tests/Unit Tests/Conditions/PostcodeConditionTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PostcodeConditionTests.swift
3 | // FormValidatorSwift
4 | //
5 | // Created by Aaron McTavish on 13/01/2016.
6 | // Copyright © 2016 ustwo. All rights reserved.
7 | //
8 |
9 | import XCTest
10 |
11 | @testable import FormValidatorSwift
12 |
13 |
14 | final class PostcodeConditionTests: XCTestCase {
15 |
16 |
17 | // MARK: - Configuration
18 |
19 | private struct TestableCondition {
20 |
21 | let country: PostcodeCountries
22 | let condition: PostcodeCondition
23 |
24 | var successInputs: [String] {
25 | switch country {
26 | case .sweden:
27 | return ["112 50",
28 | "11434",
29 | "SE-111 21",
30 | "SE-11637",
31 | "se-11637"]
32 | case .turkey:
33 | return ["34345"]
34 | case .unitedKingdom:
35 | return ["M1 1BA"]
36 | case .unitedStates:
37 | return ["20500",
38 | "95014-2083"]
39 | }
40 | }
41 |
42 | var failureInputs: [String?] {
43 | switch country {
44 | case .sweden:
45 | return ["113 4",
46 | "116233",
47 | "us-125 41",
48 | "us-125e1",
49 | nil]
50 | case .turkey:
51 | return ["3411",
52 | "347001",
53 | "34 700",
54 | "3470a",
55 | nil]
56 | case .unitedKingdom:
57 | return ["M1AA 1BA",
58 | "M1 1BAA",
59 | nil]
60 | case .unitedStates:
61 | return ["1234",
62 | "12345-1",
63 | nil]
64 | }
65 | }
66 |
67 | init(country: PostcodeCountries) {
68 | self.country = country
69 | condition = PostcodeCondition(configuration: PostcodeConfiguration(country: country))
70 | }
71 |
72 | }
73 |
74 |
75 | // MARK: - Tests
76 |
77 | func testPostcodeCondition_Success() {
78 | for country in PostcodeCountries.allValues {
79 | let testCondition = TestableCondition(country: country)
80 |
81 | for input in testCondition.successInputs {
82 | AssertCondition(testCondition.condition, testInput: input, expectedResult: true)
83 | }
84 | }
85 | }
86 |
87 | func testPostcodeCondition_Failure() {
88 | for country in PostcodeCountries.allValues {
89 | let testCondition = TestableCondition(country: country)
90 |
91 | for input in testCondition.failureInputs {
92 | AssertCondition(testCondition.condition, testInput: input, expectedResult: false)
93 | }
94 | }
95 | }
96 |
97 | func testPostCondition_ChangeCountry() {
98 | // Given
99 | var condition = PostcodeCondition()
100 |
101 | // When
102 | condition.configuration.country = .sweden
103 |
104 | // Then
105 | XCTAssertEqual(condition.configuration.country, PostcodeCountries.sweden)
106 | XCTAssertEqual(condition.regex, PostcodeCountries.sweden.regex)
107 | }
108 |
109 | }
110 |
--------------------------------------------------------------------------------
/Tests/Unit Tests/Conditions/CreditCardConditionTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CreditCardConditionTests.swift
3 | // FormValidatorSwift
4 | //
5 | // Created by Onur Ersel on 2016-11-02.
6 | // Copyright © 2016 ustwo. All rights reserved.
7 | //
8 |
9 | import XCTest
10 |
11 | @testable import FormValidatorSwift
12 |
13 |
14 | final class CreditCardConditionTests: XCTestCase {
15 |
16 |
17 | // MARK: - Properties
18 |
19 | // Conditions
20 | let conditionAll = CreditCardCondition()
21 |
22 | let cardNumberWhitespace = "3760 3171 0126 369"
23 | let cardNumberDashes = "3760-3171-0126-369"
24 | let cardNumberDigitsAndChars = "3760Das3171Uyn0126‘[ƺ369"
25 |
26 | // Invalid Card Numbers
27 | let cardNumberInvalid_1 = "0000000000000000"
28 | let cardNumberInvalid_2 = "67628 568"
29 |
30 | // Condition - Valid Card Number pairs
31 | let cardConditionsAndNumbers: [(CreditCardType, CreditCardCondition, String)] = [
32 | (CreditCardType.americanExpress, CreditCardCondition(configuration: CreditCardConfiguration(cardType: .americanExpress)), "376031710126369"),
33 | (CreditCardType.dinersClub, CreditCardCondition(configuration: CreditCardConfiguration(cardType: .dinersClub)), "30085182354725"),
34 | (CreditCardType.discover, CreditCardCondition(configuration: CreditCardConfiguration(cardType: .discover)), "6011359046468736"),
35 | (CreditCardType.jcb, CreditCardCondition(configuration: CreditCardConfiguration(cardType: .jcb)), "3535983484092395"),
36 | (CreditCardType.maestro, CreditCardCondition(configuration: CreditCardConfiguration(cardType: .maestro)), "6762856858323942"),
37 | (CreditCardType.mastercard, CreditCardCondition(configuration: CreditCardConfiguration(cardType: .mastercard)), "5480785928215247"),
38 | (CreditCardType.visa, CreditCardCondition(configuration: CreditCardConfiguration(cardType: .visa)), "4024007127428075"),
39 | ]
40 |
41 |
42 |
43 | // MARK: - Test Success
44 |
45 | func testCreditCardCondition_Success() {
46 | // Given
47 | let expectedResult = true
48 |
49 | // Test
50 | for pair in cardConditionsAndNumbers {
51 | AssertCondition(conditionAll, testInput: pair.2, expectedResult: expectedResult)
52 | }
53 |
54 | for pair in cardConditionsAndNumbers {
55 | AssertCondition(pair.1, testInput: pair.2, expectedResult: expectedResult)
56 | }
57 |
58 | for card in cardConditionsAndNumbers {
59 | for pair in cardConditionsAndNumbers {
60 | AssertCondition(card.1, testInput: pair.2, expectedResult: (pair.0 == card.0))
61 | }
62 | }
63 |
64 |
65 |
66 | AssertCondition(conditionAll, testInput: cardNumberWhitespace, expectedResult: expectedResult)
67 | AssertCondition(conditionAll, testInput: cardNumberDashes, expectedResult: expectedResult)
68 | AssertCondition(conditionAll, testInput: cardNumberDigitsAndChars, expectedResult: expectedResult)
69 | }
70 |
71 |
72 | // MARK: - Test Failure
73 |
74 | func testCreditCardCondition_Failure () {
75 | // Given
76 | let expectedResult = false
77 |
78 | // Test
79 | AssertCondition(conditionAll, testInput: cardNumberInvalid_1, expectedResult: expectedResult)
80 | AssertCondition(conditionAll, testInput: cardNumberInvalid_2, expectedResult: expectedResult)
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/Sources/Protocols/Form.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Form.swift
3 | // FormValidatorSwift
4 | //
5 | // Created by Aaron McTavish on 14/01/2016.
6 | // Copyright © 2016 ustwo Fampany Ltd. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 |
12 | /**
13 | * A form to assist in validating `ValidatorControl` objects' current states.
14 | */
15 | public protocol Form {
16 | // MARK: - Properties
17 |
18 | /// Entries in the form.
19 | var entries: [FormEntry] { get set }
20 |
21 | /// Whether or not the entire form is valid.
22 | var isValid: Bool { get }
23 |
24 |
25 | // MARK: - Initializers
26 |
27 | /**
28 | Creates an empty `Form`.
29 | */
30 | init()
31 |
32 | /**
33 | Creates a `Form` where each `Validatable` uses its own `Validator` for validation.
34 | - parameter validatables: Array of `Validatable`.
35 | */
36 | init(validatables: [ValidatorControl])
37 |
38 | /**
39 | Creates a `Form` where each `Validatable` uses a custom `Validator` for validation. If `validatables` and `validators` have a different number of elements then returns `nil`.
40 |
41 | - parameter validatables: Array of `Validatable`.
42 | - parameter validators: Array of `Validator`.
43 | */
44 | init?(validatables: [ValidatorControl], validators: [Validator])
45 |
46 |
47 | // MARK: - Manipulate Entry
48 |
49 | mutating func addEntry(_ control: ValidatorControl)
50 |
51 | mutating func removeControlAtIndex(_ index: Int) -> ValidatorControl?
52 |
53 |
54 | // MARK: - Check
55 |
56 | /**
57 | Checks the text from each entry in `entries`.
58 | - returns: An array of conditions that were violated. If no conditions were violated then `nil` is returned.
59 | */
60 | func checkConditions() -> [Condition]?
61 |
62 | }
63 |
64 |
65 | // Default implementation for `isValid`, `init(validatables:)`, `init?(validatables:validators:)`, and `checkConditions`.
66 | public extension Form {
67 |
68 |
69 | // MARK: - Properties
70 |
71 | var isValid: Bool {
72 | return checkConditions() == nil
73 | }
74 |
75 |
76 | // MARK: - Initializers
77 |
78 | init(validatables: [ValidatorControl]) {
79 | self.init()
80 | entries = validatables.map { FormEntry(validatable: $0, validator: $0.validator) }
81 | }
82 |
83 | init?(validatables: [ValidatorControl], validators: [Validator]) {
84 | guard validatables.count == validators.count else {
85 | return nil
86 | }
87 |
88 | self.init()
89 |
90 | var entries = [FormEntry]()
91 | for index in 0 ..< validatables.count {
92 | entries.append(FormEntry(validatable: validatables[index], validator: validators[index]))
93 | }
94 | self.entries = entries
95 | }
96 |
97 |
98 | // MARK: - Manipulate Entry
99 |
100 | mutating func addEntry(_ control: ValidatorControl) {
101 | entries.append(FormEntry(validatable: control, validator: control.validator))
102 | }
103 |
104 | mutating func removeControlAtIndex(_ index: Int) -> ValidatorControl? {
105 | let entry = entries.remove(at: index)
106 | return entry.validatable
107 | }
108 |
109 |
110 | // MARK: - Check
111 |
112 | func checkConditions() -> [Condition]? {
113 | let violatedConditions = entries.map { $0.checkConditions() }.filter { $0 != nil }.map { $0! }.flatMap { $0 }
114 |
115 | return violatedConditions.isEmpty ? nil : violatedConditions
116 | }
117 |
118 | }
119 |
--------------------------------------------------------------------------------
/Sources/Controls/ValidatorTextView-AppKit.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ValidatorTextView-AppKit.swift
3 | // FormValidatorSwift
4 | //
5 | // Created by Aaron McTavish on 10/01/2017.
6 | // Copyright © 2017 ustwo Fampany Ltd. All rights reserved.
7 | //
8 |
9 | import AppKit
10 |
11 |
12 | open class ValidatorTextView: NSTextView, ValidatorControl {
13 |
14 |
15 | // MARK: - Properties
16 |
17 | open var shouldAllowViolation = false
18 | open var validateOnFocusLossOnly = false
19 | public let validator: Validator
20 | open weak var validatorDelegate: ValidatorControlDelegate?
21 |
22 | open var validatableText: String? {
23 | return string
24 | }
25 |
26 | private var didEndEditing = false
27 | private var lastIsValid: Bool?
28 |
29 |
30 | // MARK: - Initializers
31 |
32 | public convenience init(validator: Validator) {
33 | self.init(frame: CGRect.zero, validator: validator)
34 | }
35 |
36 | public convenience init(frame: CGRect, validator: Validator) {
37 | self.init(frame: frame, textContainer: nil, validator: validator)
38 | }
39 |
40 | public init(frame: CGRect, textContainer: NSTextContainer?, validator: Validator) {
41 | self.validator = validator
42 |
43 | super.init(frame: frame, textContainer: textContainer)
44 | }
45 |
46 | public required init?(coder aDecoder: NSCoder) {
47 | fatalError("Not implemented.")
48 | }
49 |
50 |
51 | // MARK: - NSTextView
52 |
53 | open override func shouldChangeText(in affectedCharRange: NSRange, replacementString: String?) -> Bool {
54 | let superShouldChange = super.shouldChangeText(in: affectedCharRange, replacementString: replacementString)
55 |
56 | guard let replacementString = replacementString,
57 | let sourceText = validatableText else {
58 |
59 | return superShouldChange
60 | }
61 |
62 | let originalString = NSString(string: sourceText)
63 |
64 | let futureString = originalString.replacingCharacters(in: affectedCharRange, with: replacementString)
65 | let conditions = validator.checkConditions(futureString)
66 |
67 | if !validateOnFocusLossOnly && affectedCharRange.location != 0,
68 | let conditions = conditions,
69 | (!shouldAllowViolation || !(conditions.isEmpty || conditions[0].shouldAllowViolation)) {
70 |
71 | return false
72 | }
73 |
74 | return superShouldChange
75 | }
76 |
77 | open override func didChangeText() {
78 | super.didChangeText()
79 |
80 | defer {
81 | // Inform delegate about changes
82 | validatorDelegate?.validatorControlDidChange(self)
83 | }
84 |
85 | // Only validate if violations are allowed
86 | // Validate according to `validateOnFocusLossOnly` while editing first time or after focus loss
87 | guard shouldAllowViolation &&
88 | (!validateOnFocusLossOnly || (validateOnFocusLossOnly && didEndEditing)) else {
89 | return
90 | }
91 |
92 | let conditions = validator.checkConditions(validatableText)
93 | let isValid = conditions == nil
94 | if lastIsValid != isValid {
95 | lastIsValid = isValid
96 |
97 | // Inform delegate about valid state change
98 | validatorDelegate?.validatorControl(self, changedValidState: isValid)
99 |
100 | if !isValid {
101 | // Inform delegatate about violation
102 | validatorDelegate?.validatorControl(self, violatedConditions: conditions!)
103 | }
104 | }
105 | }
106 |
107 | }
108 |
--------------------------------------------------------------------------------
/Example/iOS/FormView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FormView.swift
3 | // FormValidatorSwift
4 | //
5 | // Created by Aaron McTavish on 14/01/2016.
6 | // Copyright © 2016 ustwo Fampany Ltd. All rights reserved.
7 | //
8 |
9 | import FormValidatorSwift
10 | import UIKit
11 |
12 |
13 | final class FormView: UIView {
14 |
15 |
16 | // MARK: - Properties
17 |
18 | let titleEntry = FormEntryView()
19 | let nameEntry = FormEntryView()
20 | let emailEntry = FormEntryView()
21 |
22 | let submitButton = UIButton(type: .system)
23 |
24 | fileprivate let bottomBufferView = UIView()
25 | fileprivate let stackView = UIStackView()
26 |
27 |
28 | // MARK: - Initializers
29 |
30 | convenience init() {
31 | self.init(frame: CGRect.zero)
32 | }
33 |
34 | override init(frame: CGRect) {
35 | super.init(frame: frame)
36 |
37 | backgroundColor = UIColor.white
38 |
39 |
40 | // Setup
41 |
42 | stackView.axis = .vertical
43 | stackView.distribution = .fill
44 | stackView.alignment = .fill
45 | addSubview(stackView)
46 |
47 | titleEntry.textLabel.text = NSLocalizedString("Title", comment: "")
48 | titleEntry.textField.shouldAllowViolation = true
49 | stackView.addArrangedSubview(titleEntry)
50 |
51 | nameEntry.textLabel.text = NSLocalizedString("Surname", comment: "")
52 | stackView.addArrangedSubview(nameEntry)
53 |
54 | emailEntry.textLabel.text = NSLocalizedString("Email", comment: "")
55 | emailEntry.textField.shouldAllowViolation = true
56 | emailEntry.textField.validateOnFocusLossOnly = true
57 | stackView.addArrangedSubview(emailEntry)
58 |
59 | submitButton.titleLabel?.font = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.body)
60 | submitButton.setTitle(NSLocalizedString("Submit", comment: ""), for: UIControl.State())
61 | stackView.addArrangedSubview(submitButton)
62 |
63 | bottomBufferView.setContentCompressionResistancePriority(UILayoutPriority.defaultLow, for: stackView.axis)
64 | bottomBufferView.setContentHuggingPriority(UILayoutPriority.defaultLow, for: stackView.axis)
65 | stackView.addArrangedSubview(bottomBufferView)
66 |
67 |
68 | // Accessibility
69 |
70 | titleEntry.textLabel.accessibilityIdentifier = FormAccessibility.Identifiers.TitleLabel
71 | titleEntry.textField.accessibilityIdentifier = FormAccessibility.Identifiers.TitleTextField
72 |
73 | nameEntry.textLabel.accessibilityIdentifier = FormAccessibility.Identifiers.NameLabel
74 | nameEntry.textField.accessibilityIdentifier = FormAccessibility.Identifiers.NameTextField
75 |
76 | emailEntry.textLabel.accessibilityIdentifier = FormAccessibility.Identifiers.EmailLabel
77 | emailEntry.textField.accessibilityIdentifier = FormAccessibility.Identifiers.EmailTextField
78 |
79 | submitButton.accessibilityIdentifier = FormAccessibility.Identifiers.SubmitButton
80 |
81 |
82 | // Layout
83 |
84 | let stackViewMargin: CGFloat = 20.0
85 | stackView.translatesAutoresizingMaskIntoConstraints = false
86 | addConstraint(NSLayoutConstraint(item: stackView, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1.0, constant: stackViewMargin))
87 | addConstraint(NSLayoutConstraint(item: stackView, attribute: .left, relatedBy: .equal, toItem: self, attribute: .left, multiplier: 1.0, constant: stackViewMargin))
88 | addConstraint(NSLayoutConstraint(item: stackView, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1.0, constant: -stackViewMargin))
89 | addConstraint(NSLayoutConstraint(item: stackView, attribute: .right, relatedBy: .equal, toItem: self, attribute: .right, multiplier: 1.0, constant: -stackViewMargin))
90 |
91 | stackView.spacing = stackViewMargin
92 | }
93 |
94 | required init?(coder aDecoder: NSCoder) {
95 | super.init(coder: aDecoder)
96 | }
97 |
98 | }
99 |
--------------------------------------------------------------------------------
/Example/iOS/FormEntryView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FormEntryView.swift
3 | // FormValidatorSwift
4 | //
5 | // Created by Aaron McTavish on 15/01/2016.
6 | // Copyright © 2016 ustwo Fampany Ltd. All rights reserved.
7 | //
8 |
9 | import FormValidatorSwift
10 | import UIKit
11 |
12 |
13 | final class FormEntryView: UIView, ValidatorControlDelegate, UITextFieldDelegate {
14 |
15 |
16 | // MARK: - Properties
17 |
18 | let textLabel = UILabel()
19 | let textField = ValidatorTextField(validator: V())
20 |
21 | let errorLabel = UILabel()
22 |
23 | fileprivate let stackView = UIStackView()
24 |
25 |
26 | // MARK: - Initializers
27 |
28 | convenience init() {
29 | self.init(frame: CGRect.zero)
30 | }
31 |
32 | override init(frame: CGRect) {
33 | super.init(frame: frame)
34 |
35 |
36 | // Setup
37 |
38 | stackView.axis = .vertical
39 | stackView.distribution = .fill
40 | stackView.alignment = .fill
41 | addSubview(stackView)
42 |
43 | textLabel.font = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.headline)
44 | textLabel.textAlignment = .center
45 | stackView.addArrangedSubview(textLabel)
46 |
47 | textField.autocorrectionType = .no
48 | textField.borderStyle = .line
49 | textField.font = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.body)
50 | textField.setValidatorDelegate(self)
51 | stackView.addArrangedSubview(textField)
52 |
53 | errorLabel.font = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.headline)
54 | errorLabel.isHidden = true
55 | errorLabel.lineBreakMode = .byWordWrapping
56 | errorLabel.numberOfLines = 0
57 | stackView.addArrangedSubview(errorLabel)
58 |
59 |
60 | // Accessibility
61 |
62 | errorLabel.accessibilityIdentifier = FormAccessibility.Identifiers.ErrorLabel
63 |
64 |
65 | // Layout
66 |
67 | let stackViewMargin: CGFloat = 0.0
68 | stackView.translatesAutoresizingMaskIntoConstraints = false
69 | addConstraint(NSLayoutConstraint(item: stackView, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1.0, constant: stackViewMargin))
70 | addConstraint(NSLayoutConstraint(item: stackView, attribute: .left, relatedBy: .equal, toItem: self, attribute: .left, multiplier: 1.0, constant: stackViewMargin))
71 | addConstraint(NSLayoutConstraint(item: stackView, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1.0, constant: -stackViewMargin))
72 | addConstraint(NSLayoutConstraint(item: stackView, attribute: .right, relatedBy: .equal, toItem: self, attribute: .right, multiplier: 1.0, constant: -stackViewMargin))
73 | }
74 |
75 | required init?(coder aDecoder: NSCoder) {
76 | super.init(coder: aDecoder)
77 | }
78 |
79 |
80 | // MARK: - ValidatorControlDelegate
81 |
82 | func validatorControl(_ validatorControl: ValidatorControl, changedValidState validState: Bool) {
83 | guard let controlView = validatorControl as? UIView else {
84 | return
85 | }
86 |
87 | if validState {
88 | controlView.layer.borderColor = nil
89 | controlView.layer.borderWidth = 0.0
90 | errorLabel.isHidden = true
91 | } else {
92 | controlView.layer.borderColor = UIColor.red.cgColor
93 | controlView.layer.borderWidth = 2.0
94 | }
95 | }
96 |
97 | func validatorControl(_ validatorControl: ValidatorControl, violatedConditions conditions: [Condition]) {
98 | var errorText = ""
99 | for condition in conditions {
100 | errorText += condition.localizedViolationString
101 | }
102 | errorLabel.text = errorText
103 |
104 | errorLabel.isHidden = false
105 | }
106 |
107 | func validatorControlDidChange(_ validatorControl: ValidatorControl) {
108 | // Not used in this example yet
109 | }
110 |
111 | }
112 |
--------------------------------------------------------------------------------
/Example/macOS/FormEntryView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FormEntryView.swift
3 | // FormValidatorSwift
4 | //
5 | // Created by Aaron McTavish on 06/01/2017.
6 | // Copyright © 2017 ustwo Fampany Ltd. All rights reserved.
7 | //
8 |
9 | import AppKit
10 |
11 | import FormValidatorSwift
12 |
13 |
14 | final class FormEntryView: NSView, ValidatorControlDelegate, NSTextFieldDelegate {
15 |
16 |
17 | // MARK: - Properties
18 |
19 | let textLabel = NSTextField()
20 | let textField = ValidatorTextField(validator: V())
21 |
22 | let errorLabel = NSTextField()
23 |
24 | fileprivate let stackView = NSStackView()
25 |
26 |
27 | // MARK: - Initializers
28 |
29 | convenience init() {
30 | self.init(frame: CGRect.zero)
31 | }
32 |
33 | override init(frame: CGRect) {
34 | super.init(frame: frame)
35 |
36 |
37 | // Setup
38 |
39 | stackView.orientation = .vertical
40 | stackView.distribution = .fill
41 | stackView.distribution = .fill
42 | addSubview(stackView)
43 |
44 | textLabel.font = NSFont.labelFont(ofSize: 12.0)
45 | textLabel.alignment = .center
46 | textLabel.isEditable = false
47 | textLabel.isBezeled = false
48 | textLabel.drawsBackground = false
49 | textLabel.isSelectable = false
50 | stackView.addArrangedSubview(textLabel)
51 |
52 | textField.font = NSFont.messageFont(ofSize: 12.0)
53 | textField.setValidatorDelegate(self)
54 | stackView.addArrangedSubview(textField)
55 |
56 | errorLabel.font = NSFont.boldSystemFont(ofSize: 14.0)
57 | errorLabel.isHidden = true
58 | errorLabel.lineBreakMode = .byWordWrapping
59 | errorLabel.isEditable = false
60 | errorLabel.isBezeled = false
61 | errorLabel.drawsBackground = false
62 | errorLabel.isSelectable = false
63 | errorLabel.maximumNumberOfLines = 2
64 | stackView.addArrangedSubview(errorLabel)
65 |
66 |
67 | // Accessibility
68 |
69 | errorLabel.setAccessibilityIdentifier(FormAccessibility.Identifiers.ErrorLabel)
70 |
71 |
72 | // Layout
73 |
74 | let stackViewMargin: CGFloat = 0.0
75 | stackView.translatesAutoresizingMaskIntoConstraints = false
76 |
77 | NSLayoutConstraint.activate([
78 | stackView.topAnchor.constraint(equalTo: topAnchor, constant: stackViewMargin),
79 | stackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -stackViewMargin),
80 | stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: stackViewMargin),
81 | stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -stackViewMargin)
82 | ])
83 | }
84 |
85 | required init?(coder aDecoder: NSCoder) {
86 | super.init(coder: aDecoder)
87 | }
88 |
89 |
90 | // MARK: - ValidatorControlDelegate
91 |
92 | func validatorControl(_ validatorControl: ValidatorControl, changedValidState validState: Bool) {
93 | guard let controlView = validatorControl as? NSView else {
94 | return
95 | }
96 |
97 | if validState {
98 | controlView.layer?.borderColor = nil
99 | controlView.layer?.borderWidth = 0.0
100 | errorLabel.isHidden = true
101 | } else {
102 | controlView.layer?.borderColor = NSColor.red.cgColor
103 | controlView.layer?.borderWidth = 2.0
104 | }
105 | }
106 |
107 | func validatorControl(_ validatorControl: ValidatorControl, violatedConditions conditions: [Condition]) {
108 | var errorText = ""
109 | for condition in conditions {
110 | errorText += condition.localizedViolationString
111 | }
112 | errorLabel.stringValue = errorText
113 |
114 | errorLabel.isHidden = false
115 | }
116 |
117 | func validatorControlDidChange(_ validatorControl: ValidatorControl) {
118 | // Not used in this example yet
119 | }
120 |
121 | }
122 |
--------------------------------------------------------------------------------
/iOS Example.xcodeproj/xcshareddata/xcschemes/iOS Example.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
34 |
40 |
41 |
42 |
43 |
44 |
50 |
51 |
52 |
53 |
54 |
55 |
65 |
67 |
73 |
74 |
75 |
76 |
77 |
78 |
84 |
86 |
92 |
93 |
94 |
95 |
97 |
98 |
101 |
102 |
103 |
--------------------------------------------------------------------------------
/FormValidatorSwift.xcodeproj/xcshareddata/xcschemes/FormValidatorSwift tvOS.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
49 |
50 |
51 |
52 |
53 |
54 |
64 |
65 |
71 |
72 |
73 |
74 |
75 |
76 |
82 |
83 |
89 |
90 |
91 |
92 |
94 |
95 |
98 |
99 |
100 |
--------------------------------------------------------------------------------
/macOS Example.xcodeproj/xcshareddata/xcschemes/macOS Example.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
34 |
40 |
41 |
42 |
43 |
44 |
50 |
51 |
52 |
53 |
54 |
55 |
65 |
67 |
73 |
74 |
75 |
76 |
77 |
78 |
84 |
86 |
92 |
93 |
94 |
95 |
97 |
98 |
101 |
102 |
103 |
--------------------------------------------------------------------------------
/FormValidatorSwift.xcodeproj/xcshareddata/xcschemes/FormValidatorSwift iOS.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
34 |
40 |
41 |
42 |
43 |
44 |
50 |
51 |
52 |
53 |
54 |
55 |
65 |
66 |
72 |
73 |
74 |
75 |
76 |
77 |
83 |
84 |
90 |
91 |
92 |
93 |
95 |
96 |
99 |
100 |
101 |
--------------------------------------------------------------------------------