Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

scratch work of sanitizers and pre/post validation #92

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,9 @@ class SSNVRule: RegexRule {
}
```

## Documentation
Checkout the docs <a href="http://jpotts18.github.io/SwiftValidator/">here</a>.

Credits
-------

Expand Down
4 changes: 2 additions & 2 deletions SwiftValidator.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "SwiftValidator"
s.version = "3.0.1"
s.version = "3.0.3"
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"
Expand All @@ -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.3" }
s.source_files = "SwiftValidator/**/*.swift"
s.exclude_files = "Validator/AppDelegate.swift"
s.frameworks = ['Foundation', 'UIKit']
Expand Down
13 changes: 11 additions & 2 deletions SwiftValidator/Core/ValidationDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,19 @@ import UIKit
This method will be called on delegate object when validation is successful.
- returns: No return value.
*/
func validationSuccessful()
optional func validationSuccessful()
/**
This method will be called on delegate object when validation fails.
- returns: No return value.
*/
func validationFailed(errors: [UITextField:ValidationError])
optional func validationFailed(errors: [UITextField:ValidationError])
/// This method is called as soon a validation starts. Should be used to do things like disable buttons, textfields once validation is started.
func willValidate()
/// This method is called just before validator's fields are validated. Should return true if validation is to be continued. Should return
/// false if validation should not run.
func shouldValidate() -> Bool
/// This method is called after validator's fields have been validated.
func didValidate()
/// This method is called after when validation does not run because preconditions have not been met.
func failedBeforeValidation()
}
29 changes: 22 additions & 7 deletions SwiftValidator/Core/Validator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public class Validator {
/// 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)?
Expand Down Expand Up @@ -62,6 +63,7 @@ public class Validator {
- returns: No return value.
*/
public func validateField(textField: UITextField, callback: (error:ValidationError?) -> Void){
// perhaps delegate should be set on validator object instead of being passed as a parameter
if let fieldRule = validations[textField] {
if let error = fieldRule.validateField() {
errors[textField] = error
Expand Down Expand Up @@ -111,10 +113,11 @@ public class Validator {
- parameter textfield: field that is to be validated.
- parameter errorLabel: A UILabel that holds error label data
- parameter rules: A Rule array that holds different rules that apply to said textField.
- paramteter sanitizers: A Sanitizer array that allows for custom cleaning of textField text.
- 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], sanitizers: [Sanitizer]? = nil) {
validations[textField] = ValidationRule(textField: textField, rules:rules, errorLabel:errorLabel, sanitizers: sanitizers)
}

/**
Expand All @@ -133,18 +136,30 @@ public class Validator {

- returns: No return value.
*/
public func validate(delegate:ValidationDelegate) {
public func validate() {
// If preconditions are not met, then automatically fail validation
if delegate!.shouldValidate() == false {
delegate!.failedBeforeValidation()
return
}

// Validation is a go so modify view controller accordingly (disable subviews)
delegate!.willValidate()

// We've made it this far, so preconditions must've been satisfied
self.validateAllFields()

if errors.isEmpty {
delegate.validationSuccessful()
// call success method if it's implemented
if delegate!.validationSuccessful?() != nil {}
} else {
delegate.validationFailed(errors)
// call failure method if it's implemented
if delegate!.validationFailed?(errors) != nil {}
}

// validation did run so update view controller (re-enable buttons and such)
delegate!.didValidate()
}

/**
This method validates all fields in validator and sets any errors to errors parameter of callback.

Expand Down
13 changes: 13 additions & 0 deletions SwiftValidator/Rules/Sanitizer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// Preparator.swift
// Validator
//
// Created by David Patterson on 2/22/16.
// Copyright © 2016 jpotts18. All rights reserved.
//

import Foundation

public protocol Sanitizer {
func sanitize(textField: UITextField)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey. I'm new here so don't want to step on any toes but I have a comment about this. I think the Sanitizer should take a string and output a string instead of taking the text field directly. This would avoid mutating state in addition to following similar api design as the Rule protocol. Additionally this would allow you to remove the sanitizers for loop and use reduce to apply the string transformations.

Additionally, I'm not sure the sanitized string should ever be reapplied back onto the text field. One example of why this isn't a good idea is if I'm trying to validate the textfield as the field is being typed in. If the sanitizer is applied (as the code is currently written) it will be changing the text field as I'm typing which could be a frustrating experience for the user.

Perhaps there should be a distinction between sanitizing text to prepare it for the rules validation process and formatting text for display purposes. In my opinion this library shouldn't be in the business of formatting the text. In that sense applying the sanitization output back on to the text field doesn't make sense.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After thinking about this some more, and looking back at the original ticket for this, I've come to the conclusion that the point of the sanitizers is to format the text. This being the case I don't feel that the sanitizers should run before validation. They should run after validation and they should only be applied on the textfields that have passed validation.

Any sanitization that needs to be done on the strings in textfields in order for rules to run would be avoiding the ultimate purpose of the rules in the first place.. to validate that the contents of the string don't violate whatever constraint that rule is trying to uphold. And if that rule doesn't care about something in the string because it doesn't violate the constraint then it should just ignore it. So why sanitize before the rule?

But this takes be back to my previous comment.. I don't think a text validation library, that markets itself as such, should be in the business of formatting text for display purposes.

}
16 changes: 16 additions & 0 deletions SwiftValidator/Rules/Sanitizers.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//
// Sanitizers.swift
// Validator
//
// Created by David Patterson on 2/22/16.
// Copyright © 2016 jpotts18. All rights reserved.
//

import Foundation

public class TrimLeadingAndTrailingSpacesSanitizer: Sanitizer {
public init() {}
public func sanitize(textField: UITextField) {
textField.text = textField.text?.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceCharacterSet())
}
}
16 changes: 14 additions & 2 deletions SwiftValidator/Rules/ValidationRule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,16 @@ import UIKit
/**
`ValidationRule` is a class that creates an object which holds validation info of a text field.
*/
public class ValidationRule {
public class ValidationRule : NSObject {
/// the text field of the field
public var textField:UITextField
/// the errorLabel of the field
public var errorLabel:UILabel?
/// the rules of the field
public var rules:[Rule] = []

public var sanitizers: [Sanitizer]?

/**
Initializes `ValidationRule` instance with text field, rules, and errorLabel.

Expand All @@ -27,17 +29,27 @@ 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?, sanitizers: [Sanitizer]? = nil){
self.textField = textField
self.errorLabel = errorLabel
self.rules = rules
self.sanitizers = sanitizers
}

/**
Used to validate text field against its validation rules.
- returns: `ValidationError` object if at least one error is found. Nil is returned if there are no validation errors.
*/
public func validateField() -> ValidationError? {
// make any preparations if there are any
if let sanitizers = sanitizers {
print("called sanitizer")
for sanitizer in sanitizers {
sanitizer.sanitize(self.textField)
}
}

// check against prepare rules before validation
return rules.filter{ !$0.validate(self.textField.text ?? "") }
.map{ rule -> ValidationError in return ValidationError(textField: self.textField, errorLabel:self.errorLabel, error: rule.errorMessage()) }.first
}
Expand Down
8 changes: 8 additions & 0 deletions Validator.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
62D1AE221A1E6D4400E4DFF8 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 62D1AE201A1E6D4400E4DFF8 /* Main.storyboard */; };
62D1AE241A1E6D4400E4DFF8 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 62D1AE231A1E6D4400E4DFF8 /* Images.xcassets */; };
62D1AE271A1E6D4400E4DFF8 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 62D1AE251A1E6D4400E4DFF8 /* LaunchScreen.xib */; };
C19BA8941C7B97CB004743FF /* Sanitizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C19BA8931C7B97CA004743FF /* Sanitizer.swift */; };
C1AB099F1C712025003C7155 /* ValidationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1AB099D1C712025003C7155 /* ValidationDelegate.swift */; };
C1B7C0FD1C7BF6A200BA7174 /* Sanitizers.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1B7C0FC1C7BF6A200BA7174 /* Sanitizers.swift */; };
FB465CB81B9884F400398388 /* SwiftValidator.h in Headers */ = {isa = PBXBuildFile; fileRef = FB465CB71B9884F400398388 /* SwiftValidator.h */; settings = {ATTRIBUTES = (Public, ); }; };
FB465CBE1B9884F400398388 /* SwiftValidator.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FB465CB31B9884F400398388 /* SwiftValidator.framework */; };
FB465CC71B9884F400398388 /* SwiftValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB465CC61B9884F400398388 /* SwiftValidatorTests.swift */; };
Expand Down Expand Up @@ -92,7 +94,9 @@
62D1AE261A1E6D4400E4DFF8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = "<group>"; };
62D1AE2C1A1E6D4500E4DFF8 /* ValidatorTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ValidatorTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
62D1AE311A1E6D4500E4DFF8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
C19BA8931C7B97CA004743FF /* Sanitizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Sanitizer.swift; sourceTree = "<group>"; };
C1AB099D1C712025003C7155 /* ValidationDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValidationDelegate.swift; sourceTree = "<group>"; };
C1B7C0FC1C7BF6A200BA7174 /* Sanitizers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Sanitizers.swift; sourceTree = "<group>"; };
FB465CB31B9884F400398388 /* SwiftValidator.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SwiftValidator.framework; sourceTree = BUILT_PRODUCTS_DIR; };
FB465CB61B9884F400398388 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
FB465CB71B9884F400398388 /* SwiftValidator.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SwiftValidator.h; sourceTree = "<group>"; };
Expand Down Expand Up @@ -262,6 +266,8 @@
FB465CEE1B9889EA00398388 /* ValidationRule.swift */,
FB465CEF1B9889EA00398388 /* ZipCodeRule.swift */,
62C1821C1C6312F5003788E7 /* ExactLengthRule.swift */,
C19BA8931C7B97CA004743FF /* Sanitizer.swift */,
C1B7C0FC1C7BF6A200BA7174 /* Sanitizers.swift */,
);
path = Rules;
sourceTree = "<group>";
Expand Down Expand Up @@ -473,6 +479,7 @@
FB465CF91B9889EA00398388 /* PasswordRule.swift in Sources */,
FB465CFD1B9889EA00398388 /* Rule.swift in Sources */,
FB465CFA1B9889EA00398388 /* PhoneNumberRule.swift in Sources */,
C1B7C0FD1C7BF6A200BA7174 /* Sanitizers.swift in Sources */,
FB465CF51B9889EA00398388 /* FloatRule.swift in Sources */,
FB465D011B9889EA00398388 /* Validator.swift in Sources */,
FB465CFE1B9889EA00398388 /* ValidationRule.swift in Sources */,
Expand All @@ -481,6 +488,7 @@
FB465CFC1B9889EA00398388 /* RequiredRule.swift in Sources */,
FB465CFB1B9889EA00398388 /* RegexRule.swift in Sources */,
FB465CF81B9889EA00398388 /* MinLengthRule.swift in Sources */,
C19BA8941C7B97CB004743FF /* Sanitizer.swift in Sources */,
C1AB099F1C712025003C7155 /* ValidationDelegate.swift in Sources */,
FB465CF71B9889EA00398388 /* MaxLengthRule.swift in Sources */,
62C1821D1C6312F5003788E7 /* ExactLengthRule.swift in Sources */,
Expand Down
10 changes: 10 additions & 0 deletions Validator/Base.lproj/Main.storyboard
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Accept" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="n3v-NX-CmA">
<rect key="frame" x="22" y="446" width="54" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="calibratedRGB"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Full Name" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="khR-OB-bgx">
<rect key="frame" x="0.0" y="-21" width="42" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
Expand Down Expand Up @@ -211,6 +217,9 @@
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits"/>
</textField>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="bim-4W-EA8">
<rect key="frame" x="84" y="441" width="51" height="31"/>
</switch>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
<constraints>
Expand Down Expand Up @@ -402,6 +411,7 @@
</variation>
</view>
<connections>
<outlet property="agreementStatus" destination="bim-4W-EA8" id="Q7q-AH-CAf"/>
<outlet property="emailConfirmErrorLabel" destination="K92-ww-iP3" id="EAZ-PN-mPh"/>
<outlet property="emailConfirmTextField" destination="O9u-O8-mPB" id="gsp-WL-cSi"/>
<outlet property="emailErrorLabel" destination="v4N-fz-1u1" id="aOl-vh-2bv"/>
Expand Down
34 changes: 29 additions & 5 deletions Validator/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class ViewController: UIViewController , ValidationDelegate, UITextFieldDelegate
@IBOutlet weak var phoneNumberErrorLabel: UILabel!
@IBOutlet weak var zipcodeErrorLabel: UILabel!
@IBOutlet weak var emailConfirmErrorLabel: UILabel!
@IBOutlet weak var agreementStatus: UISwitch!

let validator = Validator()

Expand All @@ -34,35 +35,39 @@ class ViewController: UIViewController , ValidationDelegate, UITextFieldDelegate
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: "hideKeyboard"))

validator.styleTransformers(success:{ (validationRule) -> Void in
print("here")
// clear error label
validationRule.errorLabel?.hidden = true
validationRule.errorLabel?.text = ""
validationRule.textField.layer.borderColor = UIColor.greenColor().CGColor
validationRule.textField.layer.borderWidth = 0.5

}, error:{ (validationError) -> Void in
print("error")
validationError.errorLabel?.hidden = false
validationError.errorLabel?.text = validationError.errorMessage
validationError.textField.layer.borderColor = UIColor.redColor().CGColor
validationError.textField.layer.borderWidth = 1.0
})

validator.registerField(fullNameTextField, errorLabel: fullNameErrorLabel , rules: [RequiredRule(), FullNameRule()])
validator.registerField(fullNameTextField, errorLabel: fullNameErrorLabel , rules: [RequiredRule(), FullNameRule()], sanitizers: [TrimLeadingAndTrailingSpacesSanitizer()])
validator.registerField(emailTextField, errorLabel: emailErrorLabel, rules: [RequiredRule(), EmailRule()])
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()])
// Set validator delegate
validator.delegate = self
}

@IBAction func submitTapped(sender: AnyObject) {
print("Validating...")
validator.validate(self)
validator.validate()
}

// MARK: ValidationDelegate Methods

func willValidate() {
print("Prepare validator for validation")
}

func validationSuccessful() {
print("Validation Success!")
let alert = UIAlertController(title: "Success", message: "You are validated!", preferredStyle: UIAlertControllerStyle.Alert)
Expand All @@ -71,10 +76,30 @@ class ViewController: UIViewController , ValidationDelegate, UITextFieldDelegate
self.presentViewController(alert, animated: true, completion: nil)

}

func validationFailed(errors:[UITextField:ValidationError]) {
print("Validation FAILED!")
}

func shouldValidate() -> Bool {
// Allow user to check that any preconditions are met before validation
// Good place to validate things other than UITextField
if !agreementStatus.on {
return false
}
return true
}

func failedBeforeValidation() {
// perform any style transformations
print("validation failed before running")
}

func didValidate() {
// perform custom post validation
print("validationDidRun called")
}

func hideKeyboard(){
self.view.endEditing(true)
}
Expand All @@ -91,5 +116,4 @@ class ViewController: UIViewController , ValidationDelegate, UITextFieldDelegate
}
return true
}

}