From 43b1861347f313092211cb08377d686fb0dfc200 Mon Sep 17 00:00:00 2001 From: Grigory Date: Thu, 8 Jun 2017 14:18:52 +0300 Subject: [PATCH] formTableView --- LeadKitAdditions.podspec | 4 +- .../Protocols/CellFieldJumpingProtocol.swift | 38 +++++ .../Protocols/CellFieldMaskProtocol.swift | 14 ++ .../CellFieldValidationProtocol.swift | 5 + .../Protocols/CellFieldsToolBarProtocol.swift | 16 ++ .../Protocols/FormCellViewModelProtocol.swift | 15 ++ .../Services/CellFieldsJumpingService.swift | 154 ++++++++++++++++++ .../Sources/Services/MaskFieldTextProxy.swift | 53 ++++++ .../ValidationService/ValidationError.swift | 15 ++ .../ValidationService/ValidationItem.swift | 98 +++++++++++ .../ValidationService/ValidationService.swift | 104 ++++++++++++ .../Views/CellTextField/CellTextField.swift | 44 +++++ .../CellTextFieldViewModel.swift | 30 ++++ 13 files changed, 589 insertions(+), 1 deletion(-) create mode 100644 LeadKitAdditions/Sources/Protocols/CellFieldJumpingProtocol.swift create mode 100644 LeadKitAdditions/Sources/Protocols/CellFieldMaskProtocol.swift create mode 100644 LeadKitAdditions/Sources/Protocols/CellFieldValidationProtocol.swift create mode 100644 LeadKitAdditions/Sources/Protocols/CellFieldsToolBarProtocol.swift create mode 100644 LeadKitAdditions/Sources/Protocols/FormCellViewModelProtocol.swift create mode 100644 LeadKitAdditions/Sources/Services/CellFieldsJumpingService.swift create mode 100644 LeadKitAdditions/Sources/Services/MaskFieldTextProxy.swift create mode 100644 LeadKitAdditions/Sources/Services/ValidationService/ValidationError.swift create mode 100644 LeadKitAdditions/Sources/Services/ValidationService/ValidationItem.swift create mode 100644 LeadKitAdditions/Sources/Services/ValidationService/ValidationService.swift create mode 100644 LeadKitAdditions/Sources/Views/CellTextField/CellTextField.swift create mode 100644 LeadKitAdditions/Sources/Views/CellTextField/CellTextFieldViewModel.swift diff --git a/LeadKitAdditions.podspec b/LeadKitAdditions.podspec index e3efa75..8a8625d 100644 --- a/LeadKitAdditions.podspec +++ b/LeadKitAdditions.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "LeadKitAdditions" - s.version = "0.0.16" + s.version = "0.0.17" s.summary = "iOS framework with a bunch of tools for rapid development" s.homepage = "https://github.com/TouchInstinct/LeadKitAdditions" s.license = "Apache License, Version 2.0" @@ -19,6 +19,8 @@ Pod::Spec.new do |s| ss.dependency "LeadKit", '0.5.1' ss.dependency "KeychainAccess", '3.0.2' ss.dependency "IDZSwiftCommonCrypto", '0.9.1' + ss.dependency "InputMask", '2.2.5' + ss.dependency "SwiftValidator", '4.0.0' end s.subspec 'Core-iOS-Extension' do |ss| diff --git a/LeadKitAdditions/Sources/Protocols/CellFieldJumpingProtocol.swift b/LeadKitAdditions/Sources/Protocols/CellFieldJumpingProtocol.swift new file mode 100644 index 0000000..176bb81 --- /dev/null +++ b/LeadKitAdditions/Sources/Protocols/CellFieldJumpingProtocol.swift @@ -0,0 +1,38 @@ +import RxSwift +import RxCocoa +import UIKit + +typealias UIItemSettingsBlock = (UIItem) -> Void where UIItem: UIView + +protocol CellFieldJumpingProtocol: FormCellViewModelProtocol { + + var toolBar: UIToolbar? { get set } + + var shouldGoForward: PublishSubject { get } + + var shouldBecomeFirstResponder: PublishSubject { get } + var shouldResignFirstResponder: PublishSubject { get } + + var returnButtonType: UIReturnKeyType { get set } + +} + +extension CellFieldJumpingProtocol { + + func bind(for textField: UITextField, to disposeBag: DisposeBag) { + shouldResignFirstResponder.asObservable() + .observeOn(MainScheduler.instance) + .subscribe(onNext: { [weak textField] _ in + textField?.resignFirstResponder() + }) + .addDisposableTo(disposeBag) + + shouldBecomeFirstResponder.asObservable() + .observeOn(MainScheduler.instance) + .subscribe(onNext: { [weak textField] _ in + textField?.becomeFirstResponder() + }) + .addDisposableTo(disposeBag) + } + +} diff --git a/LeadKitAdditions/Sources/Protocols/CellFieldMaskProtocol.swift b/LeadKitAdditions/Sources/Protocols/CellFieldMaskProtocol.swift new file mode 100644 index 0000000..493fb62 --- /dev/null +++ b/LeadKitAdditions/Sources/Protocols/CellFieldMaskProtocol.swift @@ -0,0 +1,14 @@ +protocol CellFieldMaskProtocol { + + var haveMask: Bool { get } + var maskFieldTextProxy: MaskFieldTextProxy? { get set } + +} + +extension CellFieldMaskProtocol { + + var haveMask: Bool { + return maskFieldTextProxy != nil + } + +} diff --git a/LeadKitAdditions/Sources/Protocols/CellFieldValidationProtocol.swift b/LeadKitAdditions/Sources/Protocols/CellFieldValidationProtocol.swift new file mode 100644 index 0000000..b294adb --- /dev/null +++ b/LeadKitAdditions/Sources/Protocols/CellFieldValidationProtocol.swift @@ -0,0 +1,5 @@ +protocol CellFieldValidationProtocol { + + var validationItem: ValidationItem? { get set } + +} diff --git a/LeadKitAdditions/Sources/Protocols/CellFieldsToolBarProtocol.swift b/LeadKitAdditions/Sources/Protocols/CellFieldsToolBarProtocol.swift new file mode 100644 index 0000000..c2bf138 --- /dev/null +++ b/LeadKitAdditions/Sources/Protocols/CellFieldsToolBarProtocol.swift @@ -0,0 +1,16 @@ +import UIKit +import RxSwift + +protocol CellFieldsToolBarProtocol: class { + + var needArrows: Bool { get set } + + var canGoForward: Bool { get set } + var canGoBackward: Bool { get set } + + var shouldGoForward: PublishSubject { get } + var shouldGoBackward: PublishSubject { get } + + var shouldEndEditing: PublishSubject { get } + +} diff --git a/LeadKitAdditions/Sources/Protocols/FormCellViewModelProtocol.swift b/LeadKitAdditions/Sources/Protocols/FormCellViewModelProtocol.swift new file mode 100644 index 0000000..84a85c9 --- /dev/null +++ b/LeadKitAdditions/Sources/Protocols/FormCellViewModelProtocol.swift @@ -0,0 +1,15 @@ +import RxCocoa +import RxSwift + +protocol FormCellViewModelProtocol: class { + var isActive: Bool { get set } +} + +extension FormCellViewModelProtocol { + + func activate(_ isActive: Bool) -> Self { + self.isActive = isActive + return self + } + +} diff --git a/LeadKitAdditions/Sources/Services/CellFieldsJumpingService.swift b/LeadKitAdditions/Sources/Services/CellFieldsJumpingService.swift new file mode 100644 index 0000000..daac2c7 --- /dev/null +++ b/LeadKitAdditions/Sources/Services/CellFieldsJumpingService.swift @@ -0,0 +1,154 @@ +import RxSwift +import UIKit + +enum CellFieldsToolBarType { + case none + case `default` +} + +struct CellFieldsJumpingServiceConfig { + + var toolBarType: CellFieldsToolBarType = .default + var toolBarNeedArrows = true + + init() {} + + init(toolBarType: CellFieldsToolBarType) { + self.toolBarType = toolBarType + } + + static var `default`: CellFieldsJumpingServiceConfig { + return CellFieldsJumpingServiceConfig() + } + +} + +class CellFieldsJumpingService { + + private var disposeBag = DisposeBag() + + // MARK: - Private properties + + private var cellFields: [CellFieldJumpingProtocol] = [] + + // MARK: - Public propertries + + var config: CellFieldsJumpingServiceConfig = .default { + didSet { + configure() + } + } + + let didDone = PublishSubject() + + // MARK: - Initialization + + init() {} + + // MARK: - Public + + func removeAll() { + cellFields.removeAll() + disposeBag = DisposeBag() + } + + func add(fieled: CellFieldJumpingProtocol, shouldConfigure: Bool = true) { + add(fieleds: [fieled], shouldConfigure: shouldConfigure) + } + + func add(fieleds: [CellFieldJumpingProtocol], shouldConfigure: Bool = true) { + cellFields += fieleds + + if shouldConfigure { + configure() + } + } + + func configure() { + disposeBag = DisposeBag() + + let cellFields = self.cellFields + + cellFields + .filter { $0.isActive } + .enumerated() + .forEach { offset, field in + field.toolBar = toolBar(for: field, with: offset) + field.returnButtonType = .next + + field.shouldGoForward.asObservable() + .subscribe(onNext: { + if let nextActive = cellFields.nextActive(from: offset) { + nextActive.shouldBecomeFirstResponder.onNext() + } else { + self.didDone.onNext() + } + }) + .addDisposableTo(disposeBag) + } + + cellFields.lastActive?.returnButtonType = .done + } + + private func toolBar(for field: CellFieldJumpingProtocol, with index: Int) -> UIToolbar { + let toolBar = CellTextFieldToolBar() + toolBar.canGoForward = cellFields.nextActive(from: index) != nil + toolBar.canGoBackward = cellFields.previousActive(from: index) != nil + + toolBar.needArrows = config.toolBarNeedArrows + + toolBar.shouldGoForward.asObservable() + .subscribe(onNext: { [weak self] in + if let nextActive = self?.cellFields.nextActive(from: index) { + nextActive.shouldBecomeFirstResponder.onNext() + } else { + self?.didDone.onNext() + } + }) + .addDisposableTo(disposeBag) + + toolBar.shouldGoBackward.asObservable() + .subscribe(onNext: { [weak self] in + if let previousActive = self?.cellFields.previousActive(from: index) { + previousActive.shouldBecomeFirstResponder.onNext() + } + }) + .addDisposableTo(disposeBag) + + toolBar.shouldEndEditing.asObservable() + .subscribe(onNext: { + field.shouldResignFirstResponder.onNext() + }) + .addDisposableTo(disposeBag) + + return toolBar + } + +} + +extension Array where Element == CellFieldJumpingProtocol { + + var firstActive: CellFieldJumpingProtocol? { + return first { $0.isActive } + } + + var lastActive: CellFieldJumpingProtocol? { + return reversed().first { $0.isActive } + } + + func nextActive(from index: Int) -> CellFieldJumpingProtocol? { + for (currentIndex, item) in enumerated() where currentIndex > index && item.isActive { + return item + } + return nil + } + + func previousActive(from index: Int) -> CellFieldJumpingProtocol? { + let reversedIndex = count - index - 1 + for (currentIndex, item) in reversed().enumerated() where currentIndex > reversedIndex && item.isActive { + return item + } + return nil + } + +} diff --git a/LeadKitAdditions/Sources/Services/MaskFieldTextProxy.swift b/LeadKitAdditions/Sources/Services/MaskFieldTextProxy.swift new file mode 100644 index 0000000..67012c8 --- /dev/null +++ b/LeadKitAdditions/Sources/Services/MaskFieldTextProxy.swift @@ -0,0 +1,53 @@ +import InputMask +import RxCocoa +import RxSwift + +class MaskFieldTextProxy: NSObject { + + private var disposeBag = DisposeBag() + + let text = Variable("") + let isComplete = Variable(false) + + private(set) var field: UITextField? + + private let maskedDelegate: PolyMaskTextFieldDelegate + + init(primaryFormat: String, affineFormats: [String] = []) { + maskedDelegate = PolyMaskTextFieldDelegate(primaryFormat: primaryFormat, affineFormats: affineFormats) + + super.init() + + maskedDelegate.listener = self + } + + func configure(with field: UITextField) { + self.field = field + field.delegate = maskedDelegate + } + + private func bindData() { + disposeBag = DisposeBag() + + text.asDriver() + .distinctUntilChanged() + .drive(onNext: { [weak self] value in + guard let textField = self?.field else { + return + } + + self?.maskedDelegate.put(text: value, into: textField) + }) + .addDisposableTo(disposeBag) + } + +} + +extension MaskFieldTextProxy: MaskedTextFieldDelegateListener { + + func textField(_ textField: UITextField, didFillMandatoryCharacters complete: Bool, didExtractValue value: String) { + text.value = value + isComplete.value = complete + } + +} diff --git a/LeadKitAdditions/Sources/Services/ValidationService/ValidationError.swift b/LeadKitAdditions/Sources/Services/ValidationService/ValidationError.swift new file mode 100644 index 0000000..69c1ec6 --- /dev/null +++ b/LeadKitAdditions/Sources/Services/ValidationService/ValidationError.swift @@ -0,0 +1,15 @@ +import SwiftValidator + +struct ValidationError: Error { + + let failedRule: Rule + let errorMessage: String? + let errorHint: String? + + init(failedRule: Rule, errorMessage: String?, errorHint: String? = nil) { + self.failedRule = failedRule + self.errorMessage = errorMessage + self.errorHint = errorHint + } + +} diff --git a/LeadKitAdditions/Sources/Services/ValidationService/ValidationItem.swift b/LeadKitAdditions/Sources/Services/ValidationService/ValidationItem.swift new file mode 100644 index 0000000..d14ee84 --- /dev/null +++ b/LeadKitAdditions/Sources/Services/ValidationService/ValidationItem.swift @@ -0,0 +1,98 @@ +import SwiftValidator +import RxSwift +import RxCocoa + +enum ValidationItemState { + case initial + case correction(ValidationError) + case error(ValidationError) + case valid +} + +extension ValidationItemState { + + var isInitial: Bool { + switch self { + case .initial: + return true + default: + return false + } + } + + var isValid: Bool { + switch self { + case .valid: + return true + default: + return false + } + } + +} + +class ValidationItem { + + private let disposeBag = DisposeBag() + + private let validationStateHolder: Variable = Variable(.initial) + var validationState: ValidationItemState { + return validationStateHolder.value + } + var validationStateObservable: Observable { + return validationStateHolder.asObservable() + } + + private(set) var rules: [Rule] = [] + private var text: String? + + init(textObservable: Observable, rules: [Rule]) { + self.rules = rules + bindValue(with: textObservable) + } + + private func bindValue(with textObservable: Observable) { + textObservable + .do(onNext: { [weak self] value in + self?.text = value + }) + .filter { [weak self] _ in !(self?.validationState.isInitial ?? true)} + .subscribe(onNext: { [weak self] value in + self?.validate(text: value) + }) + .addDisposableTo(disposeBag) + } + + @discardableResult + func manualValidate() -> Bool { + return validate(text: text, isManual: true) + } + + @discardableResult + private func validate(text: String?, isManual: Bool = false) -> Bool { + let error = rules.filter{ + return !$0.validate(text ?? "") + } + .map{ rule -> ValidationError in + return ValidationError(failedRule: rule, errorMessage: rule.errorMessage()) + } + .first + + if let validationError = error { + switch validationStateHolder.value { + case .error where !isManual, + .correction where !isManual, + .valid where !isManual: + + validationStateHolder.value = .correction(validationError) + default: + validationStateHolder.value = .error(validationError) + } + } else { + validationStateHolder.value = .valid + } + + return validationStateHolder.value.isValid + } + +} diff --git a/LeadKitAdditions/Sources/Services/ValidationService/ValidationService.swift b/LeadKitAdditions/Sources/Services/ValidationService/ValidationService.swift new file mode 100644 index 0000000..5e2366a --- /dev/null +++ b/LeadKitAdditions/Sources/Services/ValidationService/ValidationService.swift @@ -0,0 +1,104 @@ +import SwiftValidator +import RxCocoa +import RxSwift + +private enum ValidationServiceStateReactType { + case none + case all + case each +} + +enum ValidationServiceState { + case initial + case valid + case invalid +} + +extension ValidationServiceState { + + var isValid: Bool { + return self == .valid + } + +} + +class ValidationService { + + private var disposeBag = DisposeBag() + + private(set) var validationItems: [ValidationItem] = [] + + private let stateHolder: Variable = Variable(.initial) + var state: ValidationServiceState { + return stateHolder.value + } + var stateObservable: Observable { + return stateHolder.asObservable() + } + + private var validationStateReactType: ValidationServiceStateReactType = .none + + func register(item: ValidationItem) { + register(items: [item]) + } + + func register(items: [ValidationItem]) { + validationItems += items + bindItems() + } + + func unregisterAll() { + validationItems.removeAll() + bindItems() + } + + func unregister(item: ValidationItem) { + unregister(items: [item]) + } + + func unregister(items: [ValidationItem]) { + items.forEach { item in + if let removeIndex = validationItems.index(where: { $0 === item }) { + validationItems.remove(at: removeIndex) + } + } + + bindItems() + } + + @discardableResult + func validate() -> Bool { + validationStateReactType = .all + let isValid = validationItems.map { $0.manualValidate()}.reduce(true) { $0 && $1 } + validationStateReactType = .each + + return isValid + } + + private func bindItems() { + disposeBag = DisposeBag() + + let allValidationStateObservables = validationItems.map { $0.validationStateObservable } + + let zipStates = Observable + .zip(allValidationStateObservables) { $0 } + .filter { [weak self] _ in self?.validationStateReactType == .all } + + let combineLatestStates = Observable + .combineLatest(allValidationStateObservables) { $0 } + .filter { [weak self] _ in self?.validationStateReactType == .each } + + let stateObservables = [zipStates, combineLatestStates] + + stateObservables.forEach { observable in + observable + .map { states -> Bool in + return states.map { $0.isValid }.reduce(true) { $0 && $1 } + } + .map { $0 ? ValidationServiceState.valid : .invalid } + .bind(to: stateHolder) + .addDisposableTo(disposeBag) + } + } + +} diff --git a/LeadKitAdditions/Sources/Views/CellTextField/CellTextField.swift b/LeadKitAdditions/Sources/Views/CellTextField/CellTextField.swift new file mode 100644 index 0000000..4ccf4ac --- /dev/null +++ b/LeadKitAdditions/Sources/Views/CellTextField/CellTextField.swift @@ -0,0 +1,44 @@ +import UIKit +import RxCocoa +import RxSwift + +class CellTextField: UITextField { + + private var disposeBag = DisposeBag() + + var viewModel: CellTextFieldViewModel? { + didSet { + configure() + } + } + + // MARK: - Init + + private func configure() { + disposeBag = DisposeBag() + + guard let viewModel = viewModel else { + return + } + + inputAccessoryView = viewModel.toolBar + returnKeyType = viewModel.returnButtonType + + text = viewModel.text.value + placeholder = viewModel.placeholder + viewModel.textFieldSettingsBlock?(self) + + viewModel.bind(for: self, to: disposeBag) + + rx.text.asDriver() + .drive(viewModel.text) + .addDisposableTo(disposeBag) + + rx.controlEvent(.editingDidEndOnExit).asObservable() + .subscribe(onNext: { + viewModel.shouldGoForward.onNext() + }) + .addDisposableTo(disposeBag) + } + +} diff --git a/LeadKitAdditions/Sources/Views/CellTextField/CellTextFieldViewModel.swift b/LeadKitAdditions/Sources/Views/CellTextField/CellTextFieldViewModel.swift new file mode 100644 index 0000000..f8088ec --- /dev/null +++ b/LeadKitAdditions/Sources/Views/CellTextField/CellTextFieldViewModel.swift @@ -0,0 +1,30 @@ +import UIKit +import RxSwift + +class CellTextFieldViewModel: CellFieldJumpingProtocol { + + let text: Variable + let placeholder: String + + let textFieldSettingsBlock: UIItemSettingsBlock? + + // MARK: - CellFieldJumpingProtocol + + var toolBar: UIToolbar? + + let shouldGoForward = PublishSubject() + + let shouldBecomeFirstResponder = PublishSubject() + let shouldResignFirstResponder = PublishSubject() + + var returnButtonType: UIReturnKeyType = .default + + var isActive: Bool = true + + init(initialText: String = "", placeholder: String = "", textFieldSettingsBlock: UIItemSettingsBlock? = nil) { + text = Variable(initialText) + self.placeholder = placeholder + self.textFieldSettingsBlock = textFieldSettingsBlock + } + +}