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( ) {
...
}
...
}
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)
}
}
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 { }