From 12571a70c7ae36e1c79f86d624899a2fca9451ac Mon Sep 17 00:00:00 2001 From: Andrew Goodwin Date: Tue, 23 Aug 2016 13:38:17 -0500 Subject: [PATCH] Added example project --- WASHD/Classes/JumpOrder.swift | 249 +++++++++++++++ WASHD/Classes/MaxLength.swift | 83 +++++ WASHD/Classes/ReplaceMe.swift | 0 WASHD/Classes/TextFormatting.swift | 208 +++++++++++++ WASHD/Classes/Validation.swift | 467 +++++++++++++++++++++++++++++ 5 files changed, 1007 insertions(+) create mode 100644 WASHD/Classes/JumpOrder.swift create mode 100644 WASHD/Classes/MaxLength.swift delete mode 100644 WASHD/Classes/ReplaceMe.swift create mode 100644 WASHD/Classes/TextFormatting.swift create mode 100644 WASHD/Classes/Validation.swift diff --git a/WASHD/Classes/JumpOrder.swift b/WASHD/Classes/JumpOrder.swift new file mode 100644 index 0000000..4b16a4b --- /dev/null +++ b/WASHD/Classes/JumpOrder.swift @@ -0,0 +1,249 @@ +// +// JumpOrder.swift +// UITextFieldExtensions +// +// Created by Andrew Goodwin on 8/19/16. +// Copyright © 2016 Andrew Goodwin. All rights reserved. +// + +import Foundation +import UIKit + +private struct AssociatedKeys { + static var fri = "jumpOrder" + static var fra = "firstResponderArray" + static var cfri = "currentjumpOrder" + static var mnd = "moveNextDate" + static var mnt = "moveNextTimer" + static var fj = "formatJump" + static var lj = "lengthJump" + static var fn = "formatNotification" + static var ln = "lengthNotification" + static var tfn = "textFormatNotification" + static var tln = "textLengthNotification" +} + +@IBDesignable +extension UITextField +{ + @IBInspectable + var formatJump: Bool { + get { + return objc_getAssociatedObject(self, &AssociatedKeys.fj) as? Bool ?? false + } + set { + objc_setAssociatedObject(self, &AssociatedKeys.fj, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + + @IBInspectable + var lengthJump: Bool { + get { + return objc_getAssociatedObject(self, &AssociatedKeys.lj) as? Bool ?? false + } + set { + objc_setAssociatedObject(self, &AssociatedKeys.lj, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + + var textLengthNotification: AnyObject? { + get { + return objc_getAssociatedObject(self, &AssociatedKeys.tln) ?? nil + } + set { + objc_setAssociatedObject(self, &AssociatedKeys.tln, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + + var textFormatNotification: AnyObject? { + get { + return objc_getAssociatedObject(self, &AssociatedKeys.tfn) ?? nil + } + set { + objc_setAssociatedObject(self, &AssociatedKeys.tfn, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + + @IBInspectable + var jumpOrder: Int { + get { + return objc_getAssociatedObject(self, &AssociatedKeys.fri) as? Int ?? -1 + } + set { + if textFormatNotification != nil{ + NSNotificationCenter.defaultCenter().removeObserver(textFormatNotification!) + } + textFormatNotification = NSNotificationCenter.defaultCenter().addObserverForName("text.MoveNext.Format", object: nil, queue: NSOperationQueue.mainQueue()) { [weak self](notification) in + if self!.formatJump{ + NSNotificationCenter.defaultCenter().postNotificationName("firstResponder.MoveNext.Format", object: nil) + } + } + if textLengthNotification != nil{ + NSNotificationCenter.defaultCenter().removeObserver(textLengthNotification!) + } + textLengthNotification = NSNotificationCenter.defaultCenter().addObserverForName("text.MoveNext.MaxLength", object: nil, queue: NSOperationQueue.mainQueue()) { [weak self](notification) in + if self!.lengthJump{ + NSNotificationCenter.defaultCenter().postNotificationName("firstResponder.MoveNext.MaxLength", object: nil) + } + } + objc_setAssociatedObject(self, &AssociatedKeys.fri, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + + func getjumpOrder()->Int{ + return jumpOrder + //NSNotificationCenter.defaultCenter().postNotificationName("firstResponder.MoveNext", object: jumpOrder + 1) + } +} + +@IBDesignable +extension UITextView +{ + @IBInspectable + var jumpOrder: Int { + get { + return objc_getAssociatedObject(self, &AssociatedKeys.fri) as? Int ?? -1 + } + set { + objc_setAssociatedObject(self, &AssociatedKeys.fri, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + + func getjumpOrder()->Int{ + return jumpOrder + //NSNotificationCenter.defaultCenter().postNotificationName("firstResponder.MoveNext", object: jumpOrder + 1) + } +} + +extension UIViewController{ + + var currentjumpOrder: Int { + get { + return objc_getAssociatedObject(self, &AssociatedKeys.cfri) as? Int ?? -1 + } + set { + objc_setAssociatedObject(self, &AssociatedKeys.cfri, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + var textInputArray: [UIView] { + get { + return objc_getAssociatedObject(self, &AssociatedKeys.fra) as? [UIView] ?? [UIView]() + } + set { + objc_setAssociatedObject( + self, + &AssociatedKeys.fra, + newValue as [UIView]?, + objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC + ) + } + } + var moveNextDate: NSDate? { + get { + return objc_getAssociatedObject(self, &AssociatedKeys.mnd) as? NSDate ?? nil + } + set { + objc_setAssociatedObject(self, &AssociatedKeys.mnd, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + var moveNextTimer: NSTimer? { + get { + return objc_getAssociatedObject(self, &AssociatedKeys.mnt) as? NSTimer ?? nil + } + set { + objc_setAssociatedObject(self, &AssociatedKeys.mnt, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + var formatNotification: AnyObject? { + get { + return objc_getAssociatedObject(self, &AssociatedKeys.fn) ?? nil + } + set { + objc_setAssociatedObject(self, &AssociatedKeys.fn, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + var lengthNotification: AnyObject? { + get { + return objc_getAssociatedObject(self, &AssociatedKeys.ln) ?? nil + } + set { + objc_setAssociatedObject(self, &AssociatedKeys.ln, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + + func moveNext(){ + self.moveNextDate = NSDate() + for view in self.textInputArray{ + if view is UITextField{ + let tf = view as! UITextField + if tf.jumpOrder == currentjumpOrder + 1{ + tf.becomeFirstResponder() + currentjumpOrder = tf.jumpOrder + break + } + } + else if view is UITextView{ + let tf = view as! UITextView + if tf.jumpOrder == currentjumpOrder + 1{ + tf.becomeFirstResponder() + currentjumpOrder = tf.jumpOrder + break + } + } + } + } + + func findAllTextInputs(){ + self.textInputArray = [UIView]() + if formatNotification != nil{ + NSNotificationCenter.defaultCenter().removeObserver(formatNotification!) + } + formatNotification = NSNotificationCenter.defaultCenter().addObserverForName("firstResponder.MoveNext.Format", object: nil, queue: NSOperationQueue.mainQueue()) { [weak self](notification) in + if self!.moveNextDate == nil{ + //nothing has fired before + self!.moveNext() + } + else if self!.moveNextDate != nil{ + print(self!.moveNextDate!.timeIntervalSinceNow) + if abs(self!.moveNextDate!.timeIntervalSinceNow) < 0.1{ + //do nothing + } + else{ + self!.moveNext() + } + } + } + if lengthNotification != nil{ + NSNotificationCenter.defaultCenter().removeObserver(lengthNotification!) + } + lengthNotification = NSNotificationCenter.defaultCenter().addObserverForName("firstResponder.MoveNext.MaxLength", object: nil, queue: NSOperationQueue.mainQueue()) { [weak self](notification) in + if self!.moveNextDate == nil{ + //nothing has fired before + self!.moveNext() + } + else if self!.moveNextDate != nil{ + print(self!.moveNextDate!.timeIntervalSinceNow) + if abs(self!.moveNextDate!.timeIntervalSinceNow) < 0.1{ + //do nothing + } + else{ + self!.moveNext() + } + } + } + findAllTextInputsInView(self.view) + } + + private func findAllTextInputsInView(inputView:UIView){ + for view in inputView.subviews{ + if view.respondsToSelector(Selector("getjumpOrder")){ + if view is UITextField{ + textInputArray.append(view) + if view.subviews.count > 0{ + findAllTextInputsInView(view) + } + } + } + } + } +} diff --git a/WASHD/Classes/MaxLength.swift b/WASHD/Classes/MaxLength.swift new file mode 100644 index 0000000..c54ac27 --- /dev/null +++ b/WASHD/Classes/MaxLength.swift @@ -0,0 +1,83 @@ +// +// Maxlength.swift +// +// Created by Andrew Goodwin on 8/16/16. +// Copyright © 2016 Conway Corporation. All rights reserved. +// + +import Foundation +import UIKit + +private struct AssociatedKeys { + static var len = "length" +} + +@IBDesignable +extension UITextField +{ + @IBInspectable + var maxLength: Int { + get { + return objc_getAssociatedObject(self, &AssociatedKeys.len) as? Int ?? Int.max + } + set { + objc_setAssociatedObject(self, &AssociatedKeys.len, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + + func reachedMaxLength(range:NSRange, string:String)->Bool{ + let oldLength = self.text!.characters.count + let replacementLength = string.characters.count + let rangeLength = range.length + let newLength = oldLength - rangeLength + replacementLength + + let returnKey = string.rangeOfString("\n") != nil + let isBackspace = string == "" + let shouldAllow = newLength <= self.maxLength || returnKey || isBackspace + if self.maxLength == newLength{ + //do i support auto first responder + NSTimer.scheduledTimerWithTimeInterval(0.05, target: self, selector: #selector(goNext), userInfo: nil, repeats: false) + } + return !shouldAllow + } + + @objc private func goNext(){ + NSNotificationCenter.defaultCenter().postNotificationName("text.MoveNext.MaxLength", object: nil) + } + +} + +@IBDesignable +extension UITextView +{ + @IBInspectable + var maxLength: Int { + get { + return objc_getAssociatedObject(self, &AssociatedKeys.len) as? Int ?? Int.max + } + set { + objc_setAssociatedObject(self, &AssociatedKeys.len, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + + func reachedMaxLength(range:NSRange, string:String)->Bool{ + let oldLength = self.text!.characters.count + let replacementLength = string.characters.count + let rangeLength = range.length + let newLength = oldLength - rangeLength + replacementLength + + let returnKey = string.rangeOfString("\n") != nil + let isBackspace = string == "" + let shouldAllow = newLength <= self.maxLength || returnKey || isBackspace + if self.maxLength == newLength{ + //do i support auto first responder + NSTimer.scheduledTimerWithTimeInterval(0.05, target: self, selector: #selector(goNext), userInfo: nil, repeats: false) + } + return shouldAllow + } + + @objc private func goNext(){ + NSNotificationCenter.defaultCenter().postNotificationName("text.MoveNext.MaxLength", object: nil) + } + +} diff --git a/WASHD/Classes/ReplaceMe.swift b/WASHD/Classes/ReplaceMe.swift deleted file mode 100644 index e69de29..0000000 diff --git a/WASHD/Classes/TextFormatting.swift b/WASHD/Classes/TextFormatting.swift new file mode 100644 index 0000000..447549a --- /dev/null +++ b/WASHD/Classes/TextFormatting.swift @@ -0,0 +1,208 @@ +// +// TextFormatting.swift +// +// Created by Andrew Goodwin on 8/17/16. +// Copyright © 2016 Conway Corporation. All rights reserved. +// + +import Foundation +import UIKit + +private struct AssociatedKeys { + static var format = "format" + static var fi = "fi" + static var ld = "ld" +} + +@IBDesignable +extension UITextField +{ + @IBInspectable + var format: String { + get { + return objc_getAssociatedObject(self, &AssociatedKeys.format) as? String ?? "" + } + set { + + let oldFormat = self.format + objc_setAssociatedObject( + self, + &AssociatedKeys.format, + newValue as NSString, + objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC + ) + + if self.text != nil && self.text!.characters.count > 0{ + formatChanged(oldFormat) + } + + } + } + private var textSourceArray: [String] { + get { + return objc_getAssociatedObject(self, &AssociatedKeys.ld) as? [String] ?? [] + } + set { + objc_setAssociatedObject( + self, + &AssociatedKeys.ld, + newValue as [NSString]?, + objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC + ) + } + } + + func formatText(string:String)->Bool{ + //string is the newest character to be added + if self.format != "" && self.text != nil{ + let backspace = string == "" + var textFieldIndex = self.text!.characters.count > 0 ? self.text!.characters.count - 1 : -1 + var textFieldArray = Array(self.text!.characters) + let newStringArray = Array(string.characters) + let formatArray = Array(self.format.characters) + var newText = "" + if backspace == false{ + textFieldArray = textFieldArray + newStringArray + for s in newStringArray{ + print(s) + if textSourceArray.count < formatArray.count{ + if String(formatArray[textSourceArray.count]).lowercaseString == "x"{ + newText = newText + String(textFieldArray[textFieldIndex+1]) + textFieldIndex = textFieldIndex + 1 + textSourceArray.append("u") + } + else{ + while String(formatArray[textSourceArray.count]).lowercaseString != "x"{ + if String(formatArray[textSourceArray.count]).lowercaseString == "\\"{ + newText = newText + "x" + textSourceArray.append("e") + textSourceArray.append("f") + } + else{ + newText = newText + String(formatArray[textSourceArray.count]) + textSourceArray.append("f") + } + } + newText = newText + String(textFieldArray[textFieldIndex+1]) + textFieldIndex = textFieldIndex + 1 + textSourceArray.append("u") + } + + } + else if textFieldIndex < textFieldArray.count{ + newText = newText + String(textFieldArray[textFieldIndex+1]) + textFieldIndex = textFieldIndex + 1 + } + print(textFieldArray) + + print(textSourceArray) + } + self.text = self.text! + newText + if self.text?.characters.count == formatArray.count{ + print("done formatting") + //if self.respondsToSelector(Selector("getjumpOrder")){ + //do i support auto first responder + NSNotificationCenter.defaultCenter().postNotificationName("text.MoveNext.Format", object: nil) + //} + } + return false + } + else{ + if textSourceArray.count > 0 && textFieldArray.count <= formatArray.count{ + + if textSourceArray.last! == "u"{ + textSourceArray.removeLast() + textFieldArray.removeLast() + } + + while textSourceArray.count > 0 && textSourceArray.last! != "u"{ + textSourceArray.removeLast() + if textSourceArray.count > 0 && textSourceArray.last! != "e"{ + textFieldArray.removeLast() + } + else if textSourceArray.count == 0 && textFieldArray.count > 0{ + textFieldArray.removeLast() + } + + } + print(textSourceArray) + + } + else if textFieldArray.count > 0{ + textFieldArray.removeLast() + } + + for ch in textFieldArray{ + newText = newText + String(ch) + } + + print(textFieldArray) + + self.text = newText + return false + } + } + + return true + } + + private func formatChanged(oldFormat:String?){ + var newText = "" + var enteredText = self.text + enteredText = getUserEnteredCharacters(oldFormat!) + textSourceArray = [] + var formatIndex = 0 + var enteredIndex = 0 + let enteredArray = Array(enteredText!.characters) + let formatArray = Array(self.format.characters) + while enteredIndex < enteredArray.count && formatIndex < formatArray.count{ + if String(formatArray[formatIndex]).lowercaseString == "\\"{ + textSourceArray.append("e") + textSourceArray.append("f") + newText = newText + "x" + formatIndex = formatIndex + 2 + } + else if String(formatArray[formatIndex]).lowercaseString == "x"{ + newText = newText + String(enteredArray[enteredIndex]) + enteredIndex = enteredIndex + 1 + formatIndex = formatIndex + 1 + textSourceArray.append("u") + } + else{ + newText = newText + String(formatArray[formatIndex]) + formatIndex = formatIndex + 1 + textSourceArray.append("f") + } + } + while enteredIndex < enteredArray.count{ + newText = newText + String(enteredArray[enteredIndex]) + enteredIndex = enteredIndex + 1 + } + + self.text = newText + + + } + + private func getUserEnteredCharacters(fromFormat:String) -> String{ + if fromFormat == ""{ + return self.text! + } + var enteredText = "" + let enteredArray = Array(self.text!.characters) + var userEnteredIndices = [Int]() + let _ = textSourceArray.enumerate().filter{(index, element) in + if element == "u"{ + userEnteredIndices.append(index) + return true + } + return false + } + for ent in userEnteredIndices{ + let char = String(enteredArray[ent]) + print(char) + enteredText = enteredText + char + } + return enteredText + } +} \ No newline at end of file diff --git a/WASHD/Classes/Validation.swift b/WASHD/Classes/Validation.swift new file mode 100644 index 0000000..8801c50 --- /dev/null +++ b/WASHD/Classes/Validation.swift @@ -0,0 +1,467 @@ +// +// Validation.swift +// +// Created by Ian J.D. Howerton on 8/9/16. +// Edited by Andrew Goodwin on 8/15/16 +// Copyright © 2016 Conway Corporation. All rights reserved. +// + +import Foundation +import UIKit + +let UpperCaseLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +let LowerCaseLetters = "abcdefghijklmnopqrstuvwxyz" +let AllLetters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" +let UpperCaseHex = "0123456789ABCDEF" +let LowerCaseHex = "0123456789abcdef" +let AllHex = "0123456789abcdefABCDEF" +let PositiveWholeNumbers = "0123456789" +let WholeNumbers = "-0123456789" +let PositiveFloats = "0123456789." +let Floats = "-0123456789." +let Email = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-+@.%" +let Street = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 -#.&" +let IPAddress = "0123456789." +let Money = "0123456789.$" +let Phone = "0123456789.()- " +let Zip = "0123456789-" + +class Validation +{ + var expressions = [ValidationExpression]() + static var expression = Validation() + + private init() + { + + let zip = ValidationExpression(expression: "^\\d{5}(-\\d{4})?$", description: "Zip Code",failureDescription: "Invalid Zip Code", hints: [ + ValidationRule(priority: 1, expression: "\\d{5}", failureDescription: "Zip code must be 5 characters"), + ValidationRule(priority: 0, expression: "[0-9]+", failureDescription: "Not numbers"), + ], interfaceBuilderAliases: ["zip","zip code"], transformText: { (zipcode) in + var myString = zipcode + myString = myString?.stringByReplacingOccurrencesOfString(" ", withString: "") + return myString! + }, furtherValidation:nil) + + let streetAddress = ValidationExpression(expression: "^[\\d]+\\s[- a-zA-Z\\d#.&]+$", description: "Street Address",failureDescription: "Invalid address", hints: [ + ValidationRule(priority: 0, expression: "^[\\d]+.+", failureDescription: "Invalid Street Number"), + ValidationRule(priority: 1, expression: "^[\\d]+\\s.+", failureDescription: "Needs to be of format 123 Main St"), + ValidationRule(priority: 2, expression: ".+[- a-zA-Z\\d#.&]+$", failureDescription: "Invalid Street Name") + ], interfaceBuilderAliases: ["street","address","street address"], transformText: { (address) in + var myString = address + myString = myString?.condensedWhitespace + return myString! + }, furtherValidation:nil) + + let phone = ValidationExpression(expression: "^(\\(\\d{3}\\)[-.\\s]?|\\d{3}[-.\\s]?)?\\d{3}[-.\\s]?\\d{4}$", description: "Phone number",failureDescription: "Invalid Phone number", hints: [ + ValidationRule(priority: 0, expression: "\\d{7}", failureDescription: "Too Few Numbers"), + ValidationRule(priority: 1, expression: "\\[0-9()-]", failureDescription: "Invalid Characters") + ], interfaceBuilderAliases: ["phone","phone #","phone number"], transformText:nil, furtherValidation:nil) + + let email = ValidationExpression(expression: "^[\\w.%+-]+@[\\w.-]+\\.[a-zA-Z]{2,6}$", description: "Email Address",failureDescription: "Invalid Email", hints: [ + ValidationRule(priority: 0, expression: "@.[a-zA-Z]+[.]+[a-zA-Z]+$", failureDescription: "Requires exactly one @"), + ValidationRule(priority: 1, expression: "[.]{1}[a-z]+$", failureDescription: "Requries exactly one period") + ], interfaceBuilderAliases: ["email","email address","@"], transformText:{ (email) in + var myString = email + myString = myString?.condensedWhitespace.lowercaseString + return myString! + }, furtherValidation:nil) + + let ipAddress = ValidationExpression(expression: "^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$", description: "IP address",failureDescription: "Invalid IP address", hints: nil, interfaceBuilderAliases: ["ip","ip address"], transformText:nil, furtherValidation:nil) + + let MACAddress = ValidationExpression(expression: "^([A-Fa-f\\d]{4}\\.[A-Fa-f\\d]{4}\\.[A-Fa-f\\d]{4})|([A-Fa-f\\d]{12})$", description: "MAC address",failureDescription: "Invalid MAC address", hints: nil, interfaceBuilderAliases: ["mac","mac address"], transformText:nil, furtherValidation:nil) + + let GPSCoordinate = ValidationExpression(expression: "^\\-?[\\d]{1,3}(\\.{1}\\d+)?$", description: "GPS Coordinate",failureDescription: "Invalid GPS Coordinate", hints: nil, interfaceBuilderAliases: ["gps","gps coordinate"], transformText:nil, furtherValidation:nil) + + let GPSPoint = ValidationExpression(expression: "^\\-?[\\d]{1,3}(\\.{1}\\d+)?\\,\\-?[\\d]{1,3}(\\.{1}\\d+)?$", description: "GPS Point",failureDescription: "Invalid GPS Point", hints: nil, interfaceBuilderAliases: ["gps point"], transformText:nil, furtherValidation:nil) + + let URL = ValidationExpression(expression: "^(https?:\\/\\/)?([\\da-z\\.-]+)\\.([a-z\\.]{2,6})([\\/\\w \\.-]*)*\\/?$", description: "URL",failureDescription: "Invalid URL", hints: nil, interfaceBuilderAliases: ["url","http","https","web address"], transformText:nil, furtherValidation:nil) + + let creditCard = ValidationExpression(expression: "^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\\d{3})\\d{11})$", + description: "Debit or Credit Card", + failureDescription: "Invalid card", + hints: [ValidationRule(priority: 0, expression: "\\d+", failureDescription: "Missing Numbers")], + interfaceBuilderAliases: ["card","credit card","debit card","cc"], + transformText:{ (card) in + var myString = card + myString = myString?.condensedWhitespace.stringByReplacingOccurrencesOfString(" ", withString: "") + return myString! + }, + furtherValidation:{[weak self] (card) in + if (self?.luhnTest(card))!{ + return ValidationResult(isValid: true, failureMessage: nil, transformedString: card) + } + else{ + return ValidationResult(isValid: false, failureMessage: "Card failed Luhn check", transformedString: card) + } + }) + + let money = ValidationExpression(expression: "\\$?[+-]?[0-9]{1,3}(?:,?[0-9]{3})*(?:\\.[0-9]{2})?$", description: "Money",failureDescription: "Invalid money format", hints: [ + ValidationRule(priority: 0, expression: "[.]{1}[0-9]{2}?$", failureDescription: "Invalid Decimal Value") + ], interfaceBuilderAliases: ["money","currency","$"], transformText:nil, furtherValidation:nil) + + let letters = ValidationExpression(expression: "^[a-zA-Z]+$", description: "Letters, No Spaces",failureDescription: "Not Letters Without Spaces", hints: [ + ValidationRule(priority: 0, expression: " *", failureDescription: "Spaces Detected") + ], interfaceBuilderAliases: ["letters","abc"], transformText:nil, furtherValidation:nil) + + let lettersWithSpaces = ValidationExpression(expression: "^[a-zA-Z ]+$", description: "Letters",failureDescription: "Not Letters", hints: nil, interfaceBuilderAliases: ["letters with spaces","letters spaces","a b c"], transformText:nil, furtherValidation:nil) + + let alphaNumeric = ValidationExpression(expression: "^[-\\da-zA-Z]+$", description: "Alpha Numeric, No Spaces",failureDescription: "Not alpha numeric", hints: [ + ValidationRule(priority: 0, expression: " *", failureDescription: "Spaces Detected") + ], interfaceBuilderAliases: ["alphanumeric","alphanumerics","abc123"], transformText:nil, furtherValidation:nil) + + let alphaNumericWithSpaces = ValidationExpression(expression: "^[\\s-\\da-zA-Z]+$", description: "Alpha Numeric",failureDescription: "Not alpha numeric", hints: nil, interfaceBuilderAliases: ["alphanumeric with spaces","alphanumerics with spaces","alphanumeric spaces","alphanumerics spaces","a b c 1 2 3"], transformText:nil, furtherValidation:nil) + + let positiveNumbers = ValidationExpression(expression: "^[0-9]+$", description: "Positive Numbers",failureDescription: "Non-positive numbers present", hints: nil, interfaceBuilderAliases: ["positive numbers","+"], transformText:nil, furtherValidation:nil) + + let negativeNumbers = ValidationExpression(expression: "^-[0-9]+$", description: "Negative Numbers",failureDescription: "Non-negative numbers present", hints: nil, interfaceBuilderAliases: ["negative numbers","-"], transformText:nil, furtherValidation:nil) + + let wholeNumbers = ValidationExpression(expression: "^-?[\\d]+$", description: "Whole Numbers",failureDescription: "Non-numbers present", hints: nil, interfaceBuilderAliases: ["numbers","all numbers","123","whole numbers","integers"], transformText:nil, furtherValidation:nil) + + let positiveFloats = ValidationExpression(expression: "^[\\d]*\\.*\\d*$", description: "Positive Floats",failureDescription: "Not a positive float", hints: [ + ValidationRule(priority: 0, expression: ".{1}", failureDescription: "Multiple decimals found") + ], interfaceBuilderAliases: ["positive floats","+f"], transformText:nil, furtherValidation:nil) + + let negativeFloats = ValidationExpression(expression: "^-[\\d]*\\.*\\d*$", description: "Negative Floats",failureDescription: "Not a negative float", hints: [ + ValidationRule(priority: 0, expression: ".{1}", failureDescription: "Multiple decimals found") + ], interfaceBuilderAliases: ["negative floats","-f"], transformText:nil, furtherValidation:nil) + + let allFloats = ValidationExpression(expression: "^-?[\\d]*\\.*\\d*$", description: "All Floats",failureDescription: "Non-numbers present", hints: [ + ValidationRule(priority: 0, expression: ".{1}", failureDescription: "Multiple decimals found")], interfaceBuilderAliases: ["floats","all floats","123f"], transformText:nil, furtherValidation:nil) + + let text = ValidationExpression(expression: "^[a-zA-Z'.,;:!&()\\-\\s]+$", description: "Text", failureDescription: "Non-Text characters present", hints: nil, interfaceBuilderAliases: ["text"], transformText:nil, furtherValidation:nil) + + let name = ValidationExpression(expression: "^[a-zA-Z'\\-\\s]+$", description: "Name", failureDescription: "Non-Text characters present", hints: nil, interfaceBuilderAliases: ["name"], transformText:nil, furtherValidation:nil) + + let ssn = ValidationExpression(expression: "^([0-9]{3}[-]*[0-9]{2}[-]*[0-9]{4})*$", description: "Social Security Numbers", failureDescription: "Invalid SSN", hints: nil, interfaceBuilderAliases: ["ssn", "ss#"], transformText:nil, furtherValidation:nil) + + let states = ValidationExpression(expression: "^(?:A[KLRZ]|C[AOT]|D[CE]|FL|GA|HI|I[ADLN]|K[SY]|LA|M[ADEINOST]|N[CDEHJMVY]|O[HKR]|PA|RI|S[CD]|T[NX]|UT|V[AT]|W[AIVY])*$", description: "State Abbreviations", failureDescription: "Invalid 2 character state abbreviation", hints: nil, interfaceBuilderAliases: ["state","states"], transformText:nil, furtherValidation:nil) + + expressions = [zip, streetAddress, phone, email, ipAddress, MACAddress, GPSCoordinate, GPSPoint, URL, creditCard, money, letters, lettersWithSpaces, alphaNumeric, alphaNumericWithSpaces, positiveNumbers, negativeNumbers, wholeNumbers, positiveFloats, negativeFloats, allFloats, text, ssn, states, name] + + } + + func isValid(expression: ValidationExpression, string: String) -> ValidationResult + { + return expression.validate(string) + } + + func isValid(validation: ValidationType, string: String) -> ValidationResult + { + let expression = expressions[validation.rawValue] + return expression.validate(string) + } + + func luhnTest(number: String) -> Bool{ + let noSpaceNum = number.condensedWhitespace + let reversedInts = noSpaceNum.characters.reverse().map + { + Int(String($0)) + } + return reversedInts.enumerate().reduce(0, combine: {(sum, val) in let odd = val.index % 2 == 1 + return sum + (odd ? (val.element! == 9 ? 9 : (val.element! * 2) % 9) : val.element!) + }) % 10 == 0 + } + +} + +enum ValidationType : Int +{ + case None = -1 + case Zip = 0 + case StreetAddress = 1 + case Phone = 2 + case Email = 3 + case IPAddress = 4 + case MACAddress = 5 + case GPSCoordinate = 6 + case GPSPoint = 7 + case URL = 8 + case CreditCard = 9 + case Money = 10 + case Letters = 11 + case LettersWithSpaces = 12 + case AlphaNumeric = 13 + case AlphaNumericWithSpaces = 14 + case PositiveNumbers = 15 + case NegativeNumbers = 16 + case WholeNumbers = 17 + case PositiveFloats = 18 + case NegativeFloats = 19 + case Floats = 20 + case Text = 21 + case SSN = 22 + case States = 23 + case Name = 24 +} + +private struct AssociatedKeys { + static var enumContext = "enumContext" + static var val = "validation" + static var valExpression = "validationExpression" + static var ac = "allowedChars" +} + +class ValidationExpression +{ + var hints: [ValidationRule]? + var description = "" + var expression = "" + var failureDescription = "" + var aliases: [String]? + var transformedString = "" + private var transformationClosure: ((String?) -> String)? = nil + private var furtherValidationClosure: ((String) -> ValidationResult)? = nil + + init() + { + + } + init(expression: String, description: String, failureDescription: String) + { + self.expression = expression + self.description = description + self.failureDescription = failureDescription + } + init(expression: String, description: String, failureDescription: String, hints: [ValidationRule]?, transformText: ((String?) -> String)?, furtherValidation: ((String) -> ValidationResult)?) + { + self.transformationClosure = transformText + self.expression = expression + self.description = description + self.hints = hints + self.hints = self.hints?.sort{ + item1, item2 in + return item1.priority < item2.priority + } + self.failureDescription = failureDescription + self.furtherValidationClosure = furtherValidation + } + init(expression: String, description: String, failureDescription: String, hints: [ValidationRule]?, interfaceBuilderAliases aliases:[String]?, transformText: ((String?) -> String)?, furtherValidation: ((String) -> ValidationResult)?) + { + self.transformationClosure = transformText + self.expression = expression + self.description = description + self.hints = hints + self.hints = self.hints?.sort{ + item1, item2 in + return item1.priority < item2.priority + } + self.aliases = aliases + self.failureDescription = failureDescription + self.furtherValidationClosure = furtherValidation + } + func validate(string: String) -> ValidationResult + { + self.transformedString = string + if self.transformationClosure != nil + { + self.transformedString = transformationClosure!(self.transformedString) + } + + let test = NSPredicate(format: "SELF MATCHES %@",expression) + let isValid = test.evaluateWithObject(self.transformedString) + if isValid == false + { + if hints != nil && hints?.count > 0{ + for index in 0...self.hints!.count - 1 + { + let result = self.hints![index].validate(self.transformedString) + if result.isValid == false + { + return result + } + } + } + } + + if self.furtherValidationClosure != nil{ + return furtherValidationClosure!(self.transformedString) + } + + return ValidationResult(isValid: isValid,failureMessage: isValid ? nil: failureDescription, transformedString: self.transformedString) + } +} + +class ValidationResult +{ + var isValid = false + var failureMessage : String? = nil + var transformedString : String = "" + + init(isValid: Bool, failureMessage:String?, transformedString: String) + { + self.isValid = isValid + self.failureMessage = failureMessage + self.transformedString = transformedString + } +} + +class ValidationRule +{ + var priority = 0 + var expression = "" + var failureDescription = "" + + init(priority: Int, expression: String, failureDescription: String) + { + self.priority = priority + self.expression = expression + self.failureDescription = failureDescription + } + func validate(string:String) -> ValidationResult + { + let test = NSPredicate(format: "SELF MATCHES %@",expression) + return ValidationResult(isValid: test.evaluateWithObject(string),failureMessage: failureDescription, transformedString: string) + } + +} + +@IBDesignable +extension UITextField +{ + func validate() -> ValidationResult + { + if self.validationExpression != nil{ + return Validation.expression.isValid(self.validationExpression!, string: self.text!) + } + else{ + return Validation.expression.isValid(self.validationType, string: self.text!) + } + } + func validate(validation: ValidationType) -> ValidationResult + { + return Validation.expression.isValid(validation, string: self.text!) + } + @IBInspectable + var validation: String? { + get { + return objc_getAssociatedObject(self, &AssociatedKeys.val) as? String + } + set { + if let newValue = newValue { + objc_setAssociatedObject( + self, + &AssociatedKeys.val, + newValue as NSString?, + objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC + ) + + for index in 0...Validation.expression.expressions.count - 1{ + let expression = Validation.expression.expressions[index] + if expression.aliases != nil{ + for alias in expression.aliases!{ + if newValue.condensedWhitespace.lowercaseString.stringByTrimmingCharactersInSet( + NSCharacterSet.whitespaceAndNewlineCharacterSet() + ) == alias.condensedWhitespace.lowercaseString.stringByTrimmingCharactersInSet( + NSCharacterSet.whitespaceAndNewlineCharacterSet() + ){ + self.validationType = ValidationType(rawValue: index)! + switch self.validationType { + case .Email: + self.allowedCharacters = Email + case .Phone: + self.allowedCharacters = Phone + case .Zip, .SSN: + self.allowedCharacters = Zip + case .StreetAddress: + self.allowedCharacters = Street + case .IPAddress: + self.allowedCharacters = IPAddress + case .Money: + self.allowedCharacters = Money + case .Letters, .States: + self.allowedCharacters = AllLetters + case .LettersWithSpaces: + self.allowedCharacters = AllLetters + " " + case .AlphaNumeric: + self.allowedCharacters = AllLetters + PositiveWholeNumbers + case .AlphaNumericWithSpaces: + self.allowedCharacters = AllLetters + PositiveWholeNumbers + " " + case .PositiveNumbers: + self.allowedCharacters = PositiveWholeNumbers + case .PositiveFloats: + self.allowedCharacters = PositiveFloats + case .NegativeNumbers, .WholeNumbers: + self.allowedCharacters = WholeNumbers + case .NegativeFloats, .Floats: + self.allowedCharacters = Floats + case .MACAddress: + self.allowedCharacters = AllHex + "." + case .Name: + self.allowedCharacters = AllLetters + "'" + "-" + default: + break + } + } + } + } + else{ + self.validationType = .None + } + } + } + } + } + var validationType: ValidationType { + get { + let rawvalue = objc_getAssociatedObject(self, &AssociatedKeys.enumContext) + if rawvalue == nil{ + return .None + }else{ + return ValidationType(rawValue: rawvalue as! Int)! + } + } + set { + objc_setAssociatedObject(self, &AssociatedKeys.enumContext, newValue.rawValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + var validationExpression: ValidationExpression? { + get { + let exp = objc_getAssociatedObject(self, &AssociatedKeys.valExpression) + if exp == nil{ + return nil + }else{ + return exp as? ValidationExpression + } + } + set { + objc_setAssociatedObject(self, &AssociatedKeys.valExpression, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + var allowedCharacters: String { + get { + return objc_getAssociatedObject(self, &AssociatedKeys.ac) as? String ?? "" + } + set { + objc_setAssociatedObject( + self, + &AssociatedKeys.ac, + newValue as NSString, + objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC + ) + } + } +} +extension String +{ + var condensedWhitespace: String + { + let components = self.componentsSeparatedByCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet()) + return components.filter { !$0.isEmpty }.joinWithSeparator(" ") + } + + func shouldAllow(allowedCharacters: String...) -> Bool + { + if allowedCharacters.count == 1 && allowedCharacters[0] == ""{ + return true + } + let characterSet = NSMutableCharacterSet() + + for str in allowedCharacters + { + characterSet.addCharactersInString(str) + } + + return !(self.rangeOfCharacterFromSet(characterSet.invertedSet) != nil) + } +}