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 {}