formTableView

This commit is contained in:
Grigory 2017-06-08 14:18:52 +03:00
parent 3defbf0534
commit 43b1861347
13 changed files with 589 additions and 1 deletions

View File

@ -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|

View File

@ -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)
}
}

View File

@ -0,0 +1,14 @@
protocol CellFieldMaskProtocol {
var haveMask: Bool { get }
var maskFieldTextProxy: MaskFieldTextProxy? { get set }
}
extension CellFieldMaskProtocol {
var haveMask: Bool {
return maskFieldTextProxy != nil
}
}

View File

@ -0,0 +1,5 @@
protocol CellFieldValidationProtocol {
var validationItem: ValidationItem? { get set }
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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)
}
}
}

View File

@ -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)
}
}

View File

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