Skip to content

Instantly share code, notes, and snippets.

@audrl1010
Last active March 28, 2019 05:38
Show Gist options
  • Save audrl1010/6e904bc70887f27fc15e53df9655b962 to your computer and use it in GitHub Desktop.
Save audrl1010/6e904bc70887f27fc15e53df9655b962 to your computer and use it in GitHub Desktop.
UITextField + Validator

Usage

class SignUpViewController: BaseViewController {
  let nicknameTextField = FormTextField(
    title: "닉네임",
    placeholder: "닉네임을 입력해주세요",
    rules: [.range(min: 1, max: 5, errorMessage: "잘못 입력")]
  )
  let emailTextField = FormTextField(
    title: "이메일",
    placeholder: "이메일을 입력해주세요",
    rules: [
      .email(errorMessage: "email error"),
      .range(min: 1, max: 5, errorMessage: "잘못 입력")
    ]
  )
  
  override func viewDidLoad() {
    ...
  }
  ...
}

Implement FormTextField

import UIKit
import RxSwift
import RxCocoa
import ObjectiveC

class FormTextField: UIView {
  struct ValidationError: Error {
    let message: String
  }
  enum Rule {
    case email(errorMessage: String)
    case range(min: Int, max: Int, errorMessage: String)
  }
  enum Color {
    static let titleLabelText = 0x999999.color
    static let titleLabelPlaceholder = 0xD0D0D0.color
    
    static let validedLineViewBackground = 0xFAC221.color
    static let invalidedLineViewBackground = 0xD8D8D8.color
    
    static let textFieldCursor = 0xFABF13.color
    static let textFieldText = 0x656162.color
  }
  enum Font {
    static let titleLabel = 12.systemFont.regular
    static let textField = 16.systemFont.regular
  }
  enum Metric {
    static let itemStackViewTop = 8.f
    static let itemStackViewHeight = 35.f
    
    static let lineViewTop = 12.f
    static let validedLineViewHeight = 1.0.f
    static let invalidedLineViewHeight = 0.5.f
  }
  
  fileprivate let titleLabel = UILabel().then {
    $0.numberOfLines = 1
    $0.textColor = Color.titleLabelText
    $0.font = Font.titleLabel
  }
  fileprivate let textField = UITextField().then {
    $0.textColor = Color.textFieldText
    $0.tintColor = Color.textFieldCursor
    $0.font = Font.textField
  }
  fileprivate let lineView = UIView().then {
    $0.backgroundColor = Color.invalidedLineViewBackground
  }
  fileprivate let itemStackView = UIStackView().then {
    $0.axis = .horizontal
    $0.alignment = .fill
    $0.distribution = .fill
    $0.spacing = 12.f
  }
  
  fileprivate var rangeRule: ValidationRuleLength?
  
  var disposeBag = DisposeBag()
  
  override var intrinsicContentSize: CGSize {
    var height = 0.f
    height += Font.titleLabel.lineHeight
    height += Metric.itemStackViewTop
    height += Metric.itemStackViewHeight
    height += Metric.lineViewTop
    height += Metric.validedLineViewHeight
    return CGSize(width: UIView.noIntrinsicMetric, height: height)
  }
  
  convenience init(title: String, placeholder: String, rules: [Rule]) {
    self.init(frame: .zero)
    self.titleLabel.text = title
    self.textField.placeholder = placeholder
    self.textField.delegate = self
    
    for rule in rules {
      switch rule {
      case .email(let errorMessage):
        let emailValidation = ValidationRulePattern(
          pattern: EmailValidationPattern(),
          error: ValidationError(message: errorMessage)
        )
        self.textField.validationRules.add(rule: emailValidation)
        
      case .range(let min, let max, let errorMessage):
        let rangeValidation = ValidationRuleLength(
          min: min,
          max: max,
          lengthType: .characters,
          error: ValidationError(message: errorMessage)
        )
        self.rangeRule = rangeValidation
        self.textField.validationRules.add(rule: rangeValidation)
      }
    }
  }
  
  override init(frame: CGRect) {
    super.init(frame: frame)
    
    self.addSubview(self.titleLabel)
    self.addSubview(self.itemStackView)
    self.itemStackView.addArrangedSubview(self.textField)
    self.addSubview(self.lineView)
    
    self.titleLabel.setContentHuggingPriority(.defaultHigh, for: .vertical)
    self.titleLabel.snp.makeConstraints { make in
      make.left.top.equalToSuperview()
    }
    self.itemStackView.setContentHuggingPriority(.defaultLow, for: .vertical)
    self.itemStackView.snp.makeConstraints { make in
      make.top.equalTo(self.titleLabel.snp.bottom)
      make.left.right.equalToSuperview()
      make.height.equalTo(Metric.itemStackViewHeight)
    }
    self.lineView.snp.makeConstraints { make in
      make.top.equalTo(self.itemStackView.snp.bottom).offset(Metric.lineViewTop)
      make.left.right.equalToSuperview()
      make.height.equalTo(Metric.invalidedLineViewHeight)
    }
    self.textField.rx.isValided
      .distinctUntilChanged()
      .map { $0 ? Color.validedLineViewBackground : Color.invalidedLineViewBackground }
      .bind(to: self.lineView.rx.backgroundColor)
      .disposed(by: self.disposeBag)

    self.textField.rx.isValided
      .distinctUntilChanged()
      .map { $0 ? Metric.validedLineViewHeight : Metric.invalidedLineViewHeight }
      .subscribe(onNext: { [weak self] height in
        guard let `self` = self else { return }
        self.lineView.snp.updateConstraints { make in
          make.height.equalTo(height)
        }
      })
      .disposed(by: self.disposeBag)
  }
  
  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
}

extension FormTextField: UITextFieldDelegate {
  func textField(
    _ textField: UITextField,
    shouldChangeCharactersIn range: NSRange,
    replacementString string: String
  ) -> Bool {
    guard let text = self.textField.text else { return true }
    guard let rangeRule = self.rangeRule else { return true }
    let newLength = text.count + string.count - range.length
    return newLength <= rangeRule.max
  }
}

Implement UITextField + Validator

extension Reactive where Base: UITextField {
  var errors: Observable<[Error]> {
    return self.base.rx.text.flatMap { [weak base = self.base] text -> Observable<[Error]> in
      guard let base = base else { return .empty() }
      let result = Validator.validate(
        input: text,
        rules: base.validationRules
      )
      switch result {
      case let .invalid(errors): return .just(errors)
      case .valid: return .empty()
      }
    }
  }

  var isValided: Observable<Bool> {
    return self.base.rx.text.flatMap { [weak base = self.base] text -> Observable<Bool> in
      guard let base = base else { return .just(false) }
      let result = Validator.validate(
        input: text,
        rules: base.validationRules
      )
      switch result {
      case .invalid: return .just(false)
      case .valid: return .just(true)
      }
    }
  }
}

extension UITextField: AssociatedObjectStore {}

private var validationRuleSetKey = "validationRuleSet"

extension UITextField {
  var validationRules: ValidationRuleSet<String> {
    get {
      return self.associatedObject(
        forKey: &validationRuleSetKey,
        default: ValidationRuleSet<String>()
      )
    }
    set {
      self.setAssociatedObject(newValue, forKey: &validationRuleSetKey)
    }
  }
}


protocol AssociatedObjectStore {}

extension AssociatedObjectStore {
  func associatedObject<T>(forKey key: UnsafeRawPointer) -> T? {
    return objc_getAssociatedObject(self, key) as? T
  }
  
  func associatedObject<T>(forKey key: UnsafeRawPointer, default: @autoclosure () -> T) -> T {
    if let object: T = self.associatedObject(forKey: key) {
      return object
    }
    let object = `default`()
    self.setAssociatedObject(object, forKey: key)
    return object
  }
  
  func setAssociatedObject<T>(_ object: T?, forKey key: UnsafeRawPointer) {
    objc_setAssociatedObject(self, key, object, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
  }
}

implemenet Validator

struct Validator {
  static func validate<R: ValidationRule>(input: R.InputType?, rule: R) -> ValidationResult {
    var ruleSet = ValidationRuleSet<R.InputType>()
    ruleSet.add(rule: rule)
    return Validator.validate(input: input, rules: ruleSet)
  }
  

  static func validate<T>(input: T?, rules: ValidationRuleSet<T>) -> ValidationResult {
    let errors = rules.rules
      .filter { !$0.validate(input: input) }
      .map { $0.error }
    return errors.isEmpty ? .valid : .invalid(errors)
  }
}


protocol ValidationRule {
  associatedtype InputType
  func validate(input: InputType?) -> Bool
  var error: Error { get }
}


struct AnyValidationRule<InputType>: ValidationRule {
  private let baseValidateInput: (InputType?) -> Bool
  
  let error: Error
  
  init<R: ValidationRule>(base: R) where R.InputType == InputType {
    self.baseValidateInput = base.validate
    self.error = base.error
  }
  
  func validate(input: InputType?) -> Bool {
    return baseValidateInput(input)
  }
}


enum ValidationResult {
  case valid
  case invalid([Error])
  var isValid: Bool { return self == .valid }
  
  func merge(with result: ValidationResult) -> ValidationResult {
    switch self {
    case .valid: return result
    case .invalid(let errorMessages):
      switch result {
      case .valid:
        return self
      case .invalid(let errorMessagesAnother):
        return .invalid([errorMessages, errorMessagesAnother].flatMap { $0 })
      }
    }
  }
  
  func merge(with results: [ValidationResult]) -> ValidationResult {
    return results.reduce(self) { return $0.merge(with: $1) }
  }
}

extension ValidationResult: Equatable {
  static func ==(lhs: ValidationResult, rhs: ValidationResult) -> Bool {
    switch (lhs, rhs) {
    case (.valid, .valid): return true
    case (.invalid(_), .invalid(_)): return true
    default: return false
    }
  }
}

struct ValidationRuleSet<InputType> {
  var rules = [AnyValidationRule<InputType>]()
  
  public init() { }
  
  init<R: ValidationRule>(rules: [R]) where R.InputType == InputType {
    self.rules = rules.map(AnyValidationRule.init)
  }
  
  mutating func add<R: ValidationRule>(rule: R) where R.InputType == InputType {
    let anyRule = AnyValidationRule(base: rule)
    rules.append(anyRule)
  }
}

public protocol ValidationPattern {
  var pattern: String { get }
}

struct ValidationRulePattern: ValidationRule {
  typealias InputType = String
  
  let error: Error
  let pattern: String
  
  init(pattern: String, error: Error) {
    self.pattern = pattern
    self.error = error
  }
  
  init(pattern: ValidationPattern, error: Error) {
    self.init(pattern: pattern.pattern, error: error)
  }

  func validate(input: String?) -> Bool {
    return NSPredicate(format: "SELF MATCHES %@", pattern).evaluate(with: input)
  }
}

struct EmailValidationPattern: ValidationPattern {
  var pattern: String {
    return "^[_A-Za-z0-9-+]+(\\.[_A-Za-z0-9-+]+)*@[A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)*(\\.[A-Za-z‌​]{2,})$"
  }
}

struct ValidationRuleLength: ValidationRule {

  enum LengthType {
    case characters
    case utf8
    case utf16
    case unicodeScalars
  }
  
  typealias InputType = String
  
  var error: Error

  let min: Int
  
  let max: Int
  
  let lengthType: LengthType

  init(min: Int = 0, max: Int = Int.max, lengthType: LengthType = .characters, error: Error) {
    self.min = min
    self.max = max
    self.lengthType = lengthType
    self.error = error
  }
  
  func validate(input: String?) -> Bool {
    guard let input = input else { return false }
    
    let length: Int
    switch lengthType {
    case .characters: length = input.count
    case .utf8: length = input.utf8.count
    case .utf16: length = input.utf16.count
    case .unicodeScalars: length = input.unicodeScalars.count
    }
    
    return length >= min && length <= max
  }
}

protocol Validatable {
  func validate<R: ValidationRule>(rule: R) -> ValidationResult where R.InputType == Self
  func validate(rules: ValidationRuleSet<Self>) -> ValidationResult
}

extension Validatable {
  public func validate<R: ValidationRule>(rule: R) -> ValidationResult where R.InputType == Self {
    return Validator.validate(input: self, rule: rule)
  }

  public func validate(rules: ValidationRuleSet<Self>) -> ValidationResult {
    return Validator.validate(input: self, rules: rules)
  }
}

extension String : Validatable {}
extension Int : Validatable {}
extension Double : Validatable {}
extension Float : Validatable {}
extension Array : Validatable {}
extension Date : Validatable {}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment