diff --git a/README.md b/README.md index 9dbb110..e8317d6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ SwiftValidator -=============== - +======= [![Build Status](https://travis-ci.org/jpotts18/SwiftValidator.svg?branch=travis-ci)](https://travis-ci.org/jpotts18/SwiftValidator) [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) [![codecov.io](https://codecov.io/github/jpotts18/SwiftValidator/coverage.svg?branch=master)](https://codecov.io/github/jpotts18/SwiftValidator?branch=master) Swift Validator is a rule-based validation library for Swift. @@ -146,6 +145,33 @@ class SSNVRule: RegexRule { } ``` +## Remote Validation + +Register field to `validator` with `remoteInfo` parameter set + +`validator.registerField(emailTextField, errorLabel: emailErrorLabel, rules: [RequiredRule(), EmailRule()], remoteInfo: (urlString: "http://localhost:8000/emails/", error: "Email already in use"))` + +Implement `ValidationDelegate`'s `remoteValidationRequest` method +```swift +func remoteValidationRequest(text: String, urlString: String, completion: (result: Bool) -> Void) { + // Add email to urlString + let newUrlString = "\(urlString)?email=\(text)" + YourNetworkingLibrary.request(.GET, newUrlString) { data -> Void in + + if data.httpResponse.statusCode == 404 { + // resource was not found, therefore the text (username, email, etc) is available + completion(result: true) + } + + if data.httpResponse.statusCode == 200 { + // resource was found, therefore the text (username, email, etc) is unavailable + completion(result: false) + } + } +// end of remoteValidationRequest method +} +``` + Credits ------- diff --git a/SwiftValidator.podspec b/SwiftValidator.podspec index 4e31e32..add6793 100644 --- a/SwiftValidator.podspec +++ b/SwiftValidator.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "SwiftValidator" - s.version = "3.0.1" + s.version = "3.0.4" s.summary = "A UITextField Validation library for Swift" s.homepage = "https://github.com/jpotts18/SwiftValidator" s.screenshots = "https://raw.githubusercontent.com/jpotts18/SwiftValidator/master/swift-validator-v2.gif" @@ -9,7 +9,7 @@ Pod::Spec.new do |s| s.social_media_url = "http://twitter.com/jpotts18" s.platform = :ios s.ios.deployment_target = '8.0' - s.source = { :git => "https://github.com/jpotts18/SwiftValidator.git", :tag => "3.0.1" } + s.source = { :git => "https://github.com/jpotts18/SwiftValidator.git", :tag => "3.0.4" } s.source_files = "SwiftValidator/**/*.swift" s.exclude_files = "Validator/AppDelegate.swift" s.frameworks = ['Foundation', 'UIKit'] diff --git a/SwiftValidator/Core/ValidationDelegate.swift b/SwiftValidator/Core/ValidationDelegate.swift index 7e70372..49471dc 100644 --- a/SwiftValidator/Core/ValidationDelegate.swift +++ b/SwiftValidator/Core/ValidationDelegate.swift @@ -13,13 +13,21 @@ import UIKit */ @objc public protocol ValidationDelegate { /** - This method will be called on delegate object when validation is successful. + This delegate method will be called on delegate object when validation is successful. - returns: No return value. */ func validationSuccessful() /** - This method will be called on delegate object when validation fails. + This delegae method will be called on delegate object when validation fails. - returns: No return value. */ func validationFailed(errors: [UITextField:ValidationError]) + /** + This delegate method is called on fields that require remote validation. + - parameter text: String to is sent to server to be validated. + - parameter urlString: String of url endpoint that will be used to validate text. + - parameter completion: closure that holds the result of the server validation request. Should be set to true if server validation was a success. Should return false if server validation failed. + - returns: No return value. + */ + optional func remoteValidationRequest(text: String, urlString: String, completion:(result: Bool) -> Void) } diff --git a/SwiftValidator/Core/Validator.swift b/SwiftValidator/Core/Validator.swift index a600b2d..afc7895 100644 --- a/SwiftValidator/Core/Validator.swift +++ b/SwiftValidator/Core/Validator.swift @@ -11,17 +11,19 @@ import UIKit /** Class that makes `Validator` objects. Should be added as a parameter to ViewController that will display validation fields. - */ +*/ public class Validator { /// Dictionary to hold all fields (and accompanying rules) that will undergo validation. public var validations = [UITextField:ValidationRule]() /// Dictionary to hold fields (and accompanying errors) that were unsuccessfully validated. public var errors = [UITextField:ValidationError]() /// Variable that holds success closure to display positive status of field. + public var delegate: ValidationDelegate? private var successStyleTransform:((validationRule:ValidationRule)->Void)? /// Variable that holds error closure to display negative status of field. private var errorStyleTransform:((validationError:ValidationError)->Void)? /// - returns: An initialized object, or nil if an object could not be created for some reason that would not result in an exception. + private var completedValidationsCount: Int = 0 public init(){} // MARK: Private functions @@ -53,6 +55,98 @@ public class Validator { } } + /** + This method is used to validate all fields registered to Validator. If validation is unsuccessful, + field gets added to errors dictionary. Completion closure is used to validator know when all fields + have undergone validation attempt. + - parameter completion: Bool that is set to true when all fields have experienced validation attempt. + - returns: No return value. + */ + private func validateAllFields(completion: (finished: Bool) -> Void) { + errors = [:] + + for (textField, rule) in validations { + if rule.remoteInfo != nil { + validateRemoteField(textField, completion: { status -> Void in + self.completedValidationsCount = self.completedValidationsCount + 1 + if self.completedValidationsCount == self.validations.count { + // Sends validation back to validate() + completion(finished: true) + } + }) + } else { + validateRegularField(textField) + self.completedValidationsCount = self.completedValidationsCount + 1 + if completedValidationsCount == validations.count { + // Sends validation back to validate() + completion(finished: true) + } + } + } + } + + /** + This method is used to validate a field that will need to also be validated via remote request. + - parameter textField: TextField of field that is being validated. + - parameter completion: Closure that holds the status of textField's validation. Is set to true + after remote validation has ended, regardless of whether the validation was a success or failure. + - returns: No return value. + */ + private func validateRemoteField(textField: UITextField, completion: (status: Bool) -> Void) { + if let fieldRule = validations[textField] { + // Carry on with validation as regular validation passed + if self.validateRegularField(fieldRule.textField) { + delegate!.remoteValidationRequest!(textField.text!, urlString: fieldRule.remoteInfo!.urlString, completion: { result -> Void in + if result { + if let transform = self.successStyleTransform { + transform(validationRule: fieldRule) + } + } else { + // Stop validation because remote validation failed + // Validation Failed on remote call + let error = ValidationError(textField: fieldRule.textField, errorLabel: fieldRule.errorLabel, error: fieldRule.remoteInfo!.error) + self.errors[fieldRule.textField] = error + if let transform = self.errorStyleTransform { + transform(validationError: error) + } + } + // Validation is over, so let validateAllFields(completion: (status: Bool)) know + completion(status: true) + }) + } else { + // Validation is over, so let validateAllFields(completion: (status: Bool)) know + completion(status: true) + } + + } + } + + /** + Method used to validate a regular field (non-remote). + - parameter: TextField of field that is undergoing validation + - returns: A Bool that represents whether the validation was a success or failure, returns true for the + former and false for the latter. + */ + private func validateRegularField(textField: UITextField) -> Bool { + if let fieldRule = validations[textField] { + if let error = fieldRule.validateField() { + errors[textField] = error + if let transform = self.errorStyleTransform { + transform(validationError: error) + return false + } + } else { + if let transform = self.successStyleTransform { + if fieldRule.remoteInfo == nil { + transform(validationRule: fieldRule) + } + return true + } + } + } + return false + } + // MARK: Public functions /** @@ -100,9 +194,9 @@ public class Validator { - parameter textField: field that is to be validated. - parameter Rule: An array which holds different rules to validate against textField. - returns: No return value - */ - public func registerField(textField:UITextField, rules:[Rule]) { - validations[textField] = ValidationRule(textField: textField, rules: rules, errorLabel: nil) + */ + public func registerField(textField:UITextField, rules:[Rule], remoteInfo: (String, String)? = nil) { + validations[textField] = ValidationRule(textField: textField, rules: rules, errorLabel: nil, remoteInfo: remoteInfo) } /** @@ -112,9 +206,9 @@ public class Validator { - parameter errorLabel: A UILabel that holds error label data - parameter rules: A Rule array that holds different rules that apply to said textField. - returns: No return value - */ - public func registerField(textField:UITextField, errorLabel:UILabel, rules:[Rule]) { - validations[textField] = ValidationRule(textField: textField, rules:rules, errorLabel:errorLabel) + */ + public func registerField(textField:UITextField, errorLabel:UILabel, rules:[Rule], remoteInfo: (String, String)? = nil) { + validations[textField] = ValidationRule(textField: textField, rules:rules, errorLabel:errorLabel, remoteInfo: remoteInfo) } /** @@ -122,7 +216,7 @@ public class Validator { - parameter textField: field used to locate and remove textField from validator. - returns: No return value - */ + */ public func unregisterField(textField:UITextField) { validations.removeValueForKey(textField) errors.removeValueForKey(textField) @@ -132,7 +226,7 @@ public class Validator { This method checks to see if all fields in validator are valid. - returns: No return value. - */ + */ public func validate(delegate:ValidationDelegate) { self.validateAllFields() @@ -146,12 +240,30 @@ public class Validator { } /** - This method validates all fields in validator and sets any errors to errors parameter of callback. + This method attempts to validate all fields registered to Validator(). + - returns: No return value. + */ + public func validate() { + self.validateAllFields { finished -> Void in + if self.errors.isEmpty { + // call success method if it's implemented + self.delegate!.validationSuccessful() + } else { + // call failure method if it's implemented + self.delegate!.validationFailed(self.errors) + } + // set number of completed validations back to 0 + self.completedValidationsCount = 0 + } + } + + /** + This method validates all fields in validator and sets any errors to errors parameter of closure. - parameter callback: A closure which is called with errors, a dictionary of type UITextField:ValidationError. - returns: No return value. - */ + */ public func validate(callback:(errors:[UITextField:ValidationError])->Void) -> Void { self.validateAllFields() diff --git a/SwiftValidator/Rules/ValidationRule.swift b/SwiftValidator/Rules/ValidationRule.swift index 4e5a316..b9b5a24 100644 --- a/SwiftValidator/Rules/ValidationRule.swift +++ b/SwiftValidator/Rules/ValidationRule.swift @@ -18,6 +18,9 @@ public class ValidationRule { public var errorLabel:UILabel? /// the rules of the field public var rules:[Rule] = [] + /// tuple that holds remote validatin info + public var remoteInfo: (urlString: String, error: String)? + //public var remoteURLString: String? /** Initializes `ValidationRule` instance with text field, rules, and errorLabel. @@ -27,10 +30,18 @@ public class ValidationRule { - parameter rules: array of Rule objects, which text field will be validated against. - returns: An initialized `ValidationRule` object, or nil if an object could not be created for some reason that would not result in an exception. */ - public init(textField: UITextField, rules:[Rule], errorLabel:UILabel?){ + public init(textField: UITextField, rules:[Rule], errorLabel:UILabel?, remoteURLString: String? = nil){ self.textField = textField self.errorLabel = errorLabel self.rules = rules + //self.remoteURLString = remoteURLString + } + + public init(textField: UITextField, rules:[Rule], errorLabel:UILabel?, remoteInfo: (String, String)? = nil){ + self.textField = textField + self.errorLabel = errorLabel + self.rules = rules + self.remoteInfo = remoteInfo } /** diff --git a/Validator/ViewController.swift b/Validator/ViewController.swift index f273e1a..697ec73 100644 --- a/Validator/ViewController.swift +++ b/Validator/ViewController.swift @@ -50,33 +50,58 @@ class ViewController: UIViewController , ValidationDelegate, UITextFieldDelegate }) validator.registerField(fullNameTextField, errorLabel: fullNameErrorLabel , rules: [RequiredRule(), FullNameRule()]) - validator.registerField(emailTextField, errorLabel: emailErrorLabel, rules: [RequiredRule(), EmailRule()]) + validator.registerField(emailTextField, errorLabel: emailErrorLabel, rules: [RequiredRule(), EmailRule()], remoteInfo: (urlString: "http://localhost:8000/emails/", error: "Email already in use")) validator.registerField(emailConfirmTextField, errorLabel: emailConfirmErrorLabel, rules: [RequiredRule(), ConfirmationRule(confirmField: emailTextField)]) validator.registerField(phoneNumberTextField, errorLabel: phoneNumberErrorLabel, rules: [RequiredRule(), MinLengthRule(length: 9)]) validator.registerField(zipcodeTextField, errorLabel: zipcodeErrorLabel, rules: [RequiredRule(), ZipCodeRule()]) + validator.delegate = self } @IBAction func submitTapped(sender: AnyObject) { print("Validating...") - validator.validate(self) + validator.validate() + } + + func simulateRemoteRequest(seconds: Int64, completion: (result: Bool) -> Void) { + print("Simulating \(seconds) second server request...") + // Set number of seconds before "request" is finished + let triggerTime = (Int64(NSEC_PER_SEC) * seconds) + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, triggerTime), dispatch_get_main_queue(), { () -> Void in + // For example purposes, if user does not enter David Patterson as full name, + // the email they tried will be already taken + if self.fullNameTextField.text! == "David Patterson" { + completion(result: true) + } else { + completion(result: false) + } + }) + } + + func hideKeyboard(){ + self.view.endEditing(true) } // MARK: ValidationDelegate Methods func validationSuccessful() { print("Validation Success!") - let alert = UIAlertController(title: "Success", message: "You are validated!", preferredStyle: UIAlertControllerStyle.Alert) + let alert = UIAlertController(title: "Success", message: "You are validated, \(fullNameTextField.text!)!", preferredStyle: UIAlertControllerStyle.Alert) let defaultAction = UIAlertAction(title: "OK", style: .Default, handler: nil) alert.addAction(defaultAction) self.presentViewController(alert, animated: true, completion: nil) } + func validationFailed(errors:[UITextField:ValidationError]) { print("Validation FAILED!") } - func hideKeyboard(){ - self.view.endEditing(true) + func remoteValidationRequest(text: String, urlString: String, completion: (result: Bool) -> Void) { + simulateRemoteRequest(2) { result -> Void in + // Set result to true if field was validated server-side + // Set to false if field was not validated server-side + completion(result: result) + } } // MARK: Validate single field