formTableView
This commit is contained in:
parent
3defbf0534
commit
43b1861347
|
|
@ -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|
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
import RxSwift
|
||||
import RxCocoa
|
||||
import UIKit
|
||||
|
||||
typealias UIItemSettingsBlock<UIItem> = (UIItem) -> Void where UIItem: UIView
|
||||
|
||||
protocol CellFieldJumpingProtocol: FormCellViewModelProtocol {
|
||||
|
||||
var toolBar: UIToolbar? { get set }
|
||||
|
||||
var shouldGoForward: PublishSubject<Void> { get }
|
||||
|
||||
var shouldBecomeFirstResponder: PublishSubject<Void> { get }
|
||||
var shouldResignFirstResponder: PublishSubject<Void> { 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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
protocol CellFieldMaskProtocol {
|
||||
|
||||
var haveMask: Bool { get }
|
||||
var maskFieldTextProxy: MaskFieldTextProxy? { get set }
|
||||
|
||||
}
|
||||
|
||||
extension CellFieldMaskProtocol {
|
||||
|
||||
var haveMask: Bool {
|
||||
return maskFieldTextProxy != nil
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
protocol CellFieldValidationProtocol {
|
||||
|
||||
var validationItem: ValidationItem? { get set }
|
||||
|
||||
}
|
||||
|
|
@ -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<Void> { get }
|
||||
var shouldGoBackward: PublishSubject<Void> { get }
|
||||
|
||||
var shouldEndEditing: PublishSubject<Void> { get }
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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<Void>()
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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<ValidationItemState> = Variable(.initial)
|
||||
var validationState: ValidationItemState {
|
||||
return validationStateHolder.value
|
||||
}
|
||||
var validationStateObservable: Observable<ValidationItemState> {
|
||||
return validationStateHolder.asObservable()
|
||||
}
|
||||
|
||||
private(set) var rules: [Rule] = []
|
||||
private var text: String?
|
||||
|
||||
init(textObservable: Observable<String?>, rules: [Rule]) {
|
||||
self.rules = rules
|
||||
bindValue(with: textObservable)
|
||||
}
|
||||
|
||||
private func bindValue(with textObservable: Observable<String?>) {
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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<ValidationServiceState> = Variable(.initial)
|
||||
var state: ValidationServiceState {
|
||||
return stateHolder.value
|
||||
}
|
||||
var stateObservable: Observable<ValidationServiceState> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import UIKit
|
||||
import RxSwift
|
||||
|
||||
class CellTextFieldViewModel: CellFieldJumpingProtocol {
|
||||
|
||||
let text: Variable<String?>
|
||||
let placeholder: String
|
||||
|
||||
let textFieldSettingsBlock: UIItemSettingsBlock<UITextField>?
|
||||
|
||||
// MARK: - CellFieldJumpingProtocol
|
||||
|
||||
var toolBar: UIToolbar?
|
||||
|
||||
let shouldGoForward = PublishSubject<Void>()
|
||||
|
||||
let shouldBecomeFirstResponder = PublishSubject<Void>()
|
||||
let shouldResignFirstResponder = PublishSubject<Void>()
|
||||
|
||||
var returnButtonType: UIReturnKeyType = .default
|
||||
|
||||
var isActive: Bool = true
|
||||
|
||||
init(initialText: String = "", placeholder: String = "", textFieldSettingsBlock: UIItemSettingsBlock<UITextField>? = nil) {
|
||||
text = Variable(initialText)
|
||||
self.placeholder = placeholder
|
||||
self.textFieldSettingsBlock = textFieldSettingsBlock
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue