From fabd2c30398891c3bf7cdae54c5dcb79fe6330ff Mon Sep 17 00:00:00 2001 From: Alexey Gerasimov Date: Tue, 25 Apr 2017 20:03:44 +0300 Subject: [PATCH] PassCode controller added --- LeadKitAdditions.podspec | 2 +- .../project.pbxproj | 68 +++++ .../Model/PassCodeConfiguration.swift | 45 ++++ .../PassCode/Model/PassCodeError.swift | 27 ++ .../PassCode/Model/PassCodeHolder.swift | 116 +++++++++ .../Model/PassCodeHolderProtocol.swift | 56 ++++ .../Model/PassCodeValidationResult.swift | 55 ++++ .../View/BasePassCodeViewController.swift | 239 ++++++++++++++++++ .../ViewModel/BasePassCodeViewModel.swift | 172 +++++++++++++ 9 files changed, 779 insertions(+), 1 deletion(-) create mode 100644 LeadKitAdditions/LeadKitAdditions/Controllers/PassCode/Model/PassCodeConfiguration.swift create mode 100644 LeadKitAdditions/LeadKitAdditions/Controllers/PassCode/Model/PassCodeError.swift create mode 100644 LeadKitAdditions/LeadKitAdditions/Controllers/PassCode/Model/PassCodeHolder.swift create mode 100644 LeadKitAdditions/LeadKitAdditions/Controllers/PassCode/Model/PassCodeHolderProtocol.swift create mode 100644 LeadKitAdditions/LeadKitAdditions/Controllers/PassCode/Model/PassCodeValidationResult.swift create mode 100644 LeadKitAdditions/LeadKitAdditions/Controllers/PassCode/View/BasePassCodeViewController.swift create mode 100644 LeadKitAdditions/LeadKitAdditions/Controllers/PassCode/ViewModel/BasePassCodeViewModel.swift diff --git a/LeadKitAdditions.podspec b/LeadKitAdditions.podspec index a882fff..b9c36ee 100644 --- a/LeadKitAdditions.podspec +++ b/LeadKitAdditions.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "LeadKitAdditions" - s.version = "0.0.3" + s.version = "0.0.4" s.summary = "iOS framework with a bunch of tools for rapid development" s.homepage = "https://github.com/NikAshanin/LeadKitAdditions" s.license = "Apache License, Version 2.0" diff --git a/LeadKitAdditions/LeadKitAdditions.xcodeproj/project.pbxproj b/LeadKitAdditions/LeadKitAdditions.xcodeproj/project.pbxproj index e6bbb41..b0dcbd6 100644 --- a/LeadKitAdditions/LeadKitAdditions.xcodeproj/project.pbxproj +++ b/LeadKitAdditions/LeadKitAdditions.xcodeproj/project.pbxproj @@ -21,6 +21,13 @@ EF05EDC81EAF91D500CAE7B6 /* BasePassCodeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF05EDC71EAF91D500CAE7B6 /* BasePassCodeService.swift */; }; EF05EDD21EAF9CF100CAE7B6 /* CommonCrypto.h in Headers */ = {isa = PBXBuildFile; fileRef = EF05EDD01EAF9CF100CAE7B6 /* CommonCrypto.h */; settings = {ATTRIBUTES = (Public, ); }; }; EF05EDDA1EAF9D4A00CAE7B6 /* CommonCrypto.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EF05EDCE1EAF9CF100CAE7B6 /* CommonCrypto.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; + EF05EDE11EAFA74200CAE7B6 /* BasePassCodeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF05EDE01EAFA74200CAE7B6 /* BasePassCodeViewController.swift */; }; + EF05EDE31EAFA7A600CAE7B6 /* BasePassCodeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF05EDE21EAFA7A600CAE7B6 /* BasePassCodeViewModel.swift */; }; + EF05EDE51EAFA80D00CAE7B6 /* PassCodeConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF05EDE41EAFA80D00CAE7B6 /* PassCodeConfiguration.swift */; }; + EF05EDE71EAFA87300CAE7B6 /* PassCodeValidationResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF05EDE61EAFA87300CAE7B6 /* PassCodeValidationResult.swift */; }; + EF05EDE91EAFA8A000CAE7B6 /* PassCodeHolderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF05EDE81EAFA8A000CAE7B6 /* PassCodeHolderProtocol.swift */; }; + EF05EDEB1EAFA8E600CAE7B6 /* PassCodeError.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF05EDEA1EAFA8E600CAE7B6 /* PassCodeError.swift */; }; + EF05EDED1EAFA96D00CAE7B6 /* PassCodeHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF05EDEC1EAFA96D00CAE7B6 /* PassCodeHolder.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -46,6 +53,13 @@ EF05EDD61EAF9D2900CAE7B6 /* CommonCrypto.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = CommonCrypto.xcconfig; sourceTree = ""; }; EF05EDD71EAF9D2900CAE7B6 /* iphoneos.modulemap */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = "sourcecode.module-map"; path = iphoneos.modulemap; sourceTree = ""; }; EF05EDD81EAF9D2900CAE7B6 /* iphonesimulator.modulemap */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = "sourcecode.module-map"; path = iphonesimulator.modulemap; sourceTree = ""; }; + EF05EDE01EAFA74200CAE7B6 /* BasePassCodeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BasePassCodeViewController.swift; sourceTree = ""; }; + EF05EDE21EAFA7A600CAE7B6 /* BasePassCodeViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BasePassCodeViewModel.swift; sourceTree = ""; }; + EF05EDE41EAFA80D00CAE7B6 /* PassCodeConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PassCodeConfiguration.swift; sourceTree = ""; }; + EF05EDE61EAFA87300CAE7B6 /* PassCodeValidationResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PassCodeValidationResult.swift; sourceTree = ""; }; + EF05EDE81EAFA8A000CAE7B6 /* PassCodeHolderProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PassCodeHolderProtocol.swift; sourceTree = ""; }; + EF05EDEA1EAFA8E600CAE7B6 /* PassCodeError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PassCodeError.swift; sourceTree = ""; }; + EF05EDEC1EAFA96D00CAE7B6 /* PassCodeHolder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PassCodeHolder.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -99,6 +113,7 @@ CAE698E51E968820000394B0 /* LeadKitAdditions */ = { isa = PBXGroup; children = ( + EF05EDDB1EAFA6FA00CAE7B6 /* Controllers */, CAE698EE1E968B72000394B0 /* Classes */, CAE699011E9693DE000394B0 /* Enums */, CAE698F81E968F56000394B0 /* Extensions */, @@ -159,6 +174,52 @@ path = CommonCrypto; sourceTree = ""; }; + EF05EDDB1EAFA6FA00CAE7B6 /* Controllers */ = { + isa = PBXGroup; + children = ( + EF05EDDC1EAFA72600CAE7B6 /* PassCode */, + ); + path = Controllers; + sourceTree = ""; + }; + EF05EDDC1EAFA72600CAE7B6 /* PassCode */ = { + isa = PBXGroup; + children = ( + EF05EDDD1EAFA72600CAE7B6 /* Model */, + EF05EDDE1EAFA72600CAE7B6 /* View */, + EF05EDDF1EAFA72600CAE7B6 /* ViewModel */, + ); + path = PassCode; + sourceTree = ""; + }; + EF05EDDD1EAFA72600CAE7B6 /* Model */ = { + isa = PBXGroup; + children = ( + EF05EDE41EAFA80D00CAE7B6 /* PassCodeConfiguration.swift */, + EF05EDEA1EAFA8E600CAE7B6 /* PassCodeError.swift */, + EF05EDEC1EAFA96D00CAE7B6 /* PassCodeHolder.swift */, + EF05EDE81EAFA8A000CAE7B6 /* PassCodeHolderProtocol.swift */, + EF05EDE61EAFA87300CAE7B6 /* PassCodeValidationResult.swift */, + ); + path = Model; + sourceTree = ""; + }; + EF05EDDE1EAFA72600CAE7B6 /* View */ = { + isa = PBXGroup; + children = ( + EF05EDE01EAFA74200CAE7B6 /* BasePassCodeViewController.swift */, + ); + path = View; + sourceTree = ""; + }; + EF05EDDF1EAFA72600CAE7B6 /* ViewModel */ = { + isa = PBXGroup; + children = ( + EF05EDE21EAFA7A600CAE7B6 /* BasePassCodeViewModel.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; F8A65FEC7C0EB4B93746E50F /* Pods */ = { isa = PBXGroup; children = ( @@ -352,15 +413,22 @@ buildActionMask = 2147483647; files = ( EF05EDB81EAF704800CAE7B6 /* UserDefaults+UserService.swift in Sources */, + EF05EDE11EAFA74200CAE7B6 /* BasePassCodeViewController.swift in Sources */, EF05EDC61EAF70EB00CAE7B6 /* TouchIDService.swift in Sources */, + EF05EDE31EAFA7A600CAE7B6 /* BasePassCodeViewModel.swift in Sources */, EF05EDC21EAF706200CAE7B6 /* DefaultNetworkService.swift in Sources */, EF05EDBB1EAF705500CAE7B6 /* ApiError.swift in Sources */, + EF05EDE91EAFA8A000CAE7B6 /* PassCodeHolderProtocol.swift in Sources */, + EF05EDED1EAFA96D00CAE7B6 /* PassCodeHolder.swift in Sources */, EF05EDB71EAF704800CAE7B6 /* Observable+Extensions.swift in Sources */, EF05EDC01EAF706200CAE7B6 /* ApiResponse.swift in Sources */, EF05EDBC1EAF705500CAE7B6 /* ConnectionError.swift in Sources */, + EF05EDEB1EAFA8E600CAE7B6 /* PassCodeError.swift in Sources */, EF05EDB41EAF703A00CAE7B6 /* BaseUserService.swift in Sources */, + EF05EDE51EAFA80D00CAE7B6 /* PassCodeConfiguration.swift in Sources */, EF05EDC81EAF91D500CAE7B6 /* BasePassCodeService.swift in Sources */, EF05EDC11EAF706200CAE7B6 /* BaseDateFormatter.swift in Sources */, + EF05EDE71EAFA87300CAE7B6 /* PassCodeValidationResult.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/LeadKitAdditions/LeadKitAdditions/Controllers/PassCode/Model/PassCodeConfiguration.swift b/LeadKitAdditions/LeadKitAdditions/Controllers/PassCode/Model/PassCodeConfiguration.swift new file mode 100644 index 0000000..8682581 --- /dev/null +++ b/LeadKitAdditions/LeadKitAdditions/Controllers/PassCode/Model/PassCodeConfiguration.swift @@ -0,0 +1,45 @@ +// +// Copyright (c) 2017 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +public struct PassCodeConfiguration { + + public var passCodeCharactersNumber: UInt = 4 + public var maxAttemptsLoginNumber: UInt = 5 + + public var shouldResetWhenGoBackground: Bool = true + + private init() {} + + init?(passCodeCharactersNumber: UInt) { + guard passCodeCharactersNumber > 0 else { + assertionFailure("passCodeCharactersNumber must be greater then 0") + return nil + } + self.passCodeCharactersNumber = passCodeCharactersNumber + } + + public static var defaultConfiguration: PassCodeConfiguration { + let passCodeConfiguration = PassCodeConfiguration() + return passCodeConfiguration + } + +} diff --git a/LeadKitAdditions/LeadKitAdditions/Controllers/PassCode/Model/PassCodeError.swift b/LeadKitAdditions/LeadKitAdditions/Controllers/PassCode/Model/PassCodeError.swift new file mode 100644 index 0000000..7001eb3 --- /dev/null +++ b/LeadKitAdditions/LeadKitAdditions/Controllers/PassCode/Model/PassCodeError.swift @@ -0,0 +1,27 @@ +// +// Copyright (c) 2017 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +public enum PassCodeError: Error { + case codesNotMatch + case wrongCode + case tooMuchAttempts +} diff --git a/LeadKitAdditions/LeadKitAdditions/Controllers/PassCode/Model/PassCodeHolder.swift b/LeadKitAdditions/LeadKitAdditions/Controllers/PassCode/Model/PassCodeHolder.swift new file mode 100644 index 0000000..47ba14a --- /dev/null +++ b/LeadKitAdditions/LeadKitAdditions/Controllers/PassCode/Model/PassCodeHolder.swift @@ -0,0 +1,116 @@ +// +// Copyright (c) 2017 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import RxSwift +import RxCocoa + +extension PassCodeHolderProtocol { + + public var passCodeHolderCreate: PassCodeHolderCreate? { + return self as? PassCodeHolderCreate + } + + public var passCodeHolderEnter: PassCodeHolderEnter? { + return self as? PassCodeHolderEnter + } + +} + +public class PassCodeHolderCreate: PassCodeHolderProtocol { + + public let type: PassCodeControllerType = .create + + private var firstPassCode: String? + private var secondPassCode: String? + + public var enterStep: PassCodeEnterStep { + if firstPassCode == nil { + return .first + } else { + return .second + } + } + + public var shouldValidate: Bool { + return firstPassCode != nil && secondPassCode != nil + } + + public var passCode: String? { + guard let firstPassCode = firstPassCode, let secondPassCode = secondPassCode, firstPassCode == secondPassCode else { + return nil + } + + return firstPassCode + } + + public func add(passCode: String) { + switch enterStep { + case .first: + firstPassCode = passCode + case .second: + secondPassCode = passCode + } + } + + public func validate() -> PassCodeValidationResult { + if let passCode = passCode { + return .valid(passCode) + } else { + return .inValid(.codesNotMatch) + } + } + + public func reset() { + firstPassCode = nil + secondPassCode = nil + } + +} + +public class PassCodeHolderEnter: PassCodeHolderProtocol { + + public let type: PassCodeControllerType = .enter + public let enterStep: PassCodeEnterStep = .first + + public var shouldValidate: Bool { + return passCode != nil + } + + public var passCode: String? + + public func add(passCode: String) { + self.passCode = passCode + } + + public func validate() -> PassCodeValidationResult { + if let passCode = passCode { + return .valid(passCode) + } else { + return .inValid(nil) + } + } + + public func reset() { + passCode = nil + } + +} diff --git a/LeadKitAdditions/LeadKitAdditions/Controllers/PassCode/Model/PassCodeHolderProtocol.swift b/LeadKitAdditions/LeadKitAdditions/Controllers/PassCode/Model/PassCodeHolderProtocol.swift new file mode 100644 index 0000000..8296679 --- /dev/null +++ b/LeadKitAdditions/LeadKitAdditions/Controllers/PassCode/Model/PassCodeHolderProtocol.swift @@ -0,0 +1,56 @@ +// +// Copyright (c) 2017 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +public enum PassCodeEnterStep { + case first + case second +} + +public protocol PassCodeHolderProtocol { + + var type: PassCodeControllerType { get } + var enterStep: PassCodeEnterStep { get } + + func add(passCode: String) + func reset() + + var shouldValidate: Bool { get } + var passCode: String? { get } + + func validate() -> PassCodeValidationResult + +} + +public class PassCodeHolderBuilder { + + private init() {} + + public static func build(with type: PassCodeControllerType) -> PassCodeHolderProtocol { + switch type { + case .create: + return PassCodeHolderCreate() + case .enter: + return PassCodeHolderEnter() + } + } + +} diff --git a/LeadKitAdditions/LeadKitAdditions/Controllers/PassCode/Model/PassCodeValidationResult.swift b/LeadKitAdditions/LeadKitAdditions/Controllers/PassCode/Model/PassCodeValidationResult.swift new file mode 100644 index 0000000..33bdd9b --- /dev/null +++ b/LeadKitAdditions/LeadKitAdditions/Controllers/PassCode/Model/PassCodeValidationResult.swift @@ -0,0 +1,55 @@ +// +// Copyright (c) 2017 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +public enum PassCodeValidationResult { + + case valid(String) + case inValid(PassCodeError?) + + public var isValid: Bool { + switch self { + case .valid: + return true + default: + return false + } + } + + public var passCode: String? { + switch self { + case let .valid(passCode): + return passCode + default: + return nil + } + } + + public var error: PassCodeError? { + switch self { + case let .inValid(error): + return error + default: + return nil + } + } + +} diff --git a/LeadKitAdditions/LeadKitAdditions/Controllers/PassCode/View/BasePassCodeViewController.swift b/LeadKitAdditions/LeadKitAdditions/Controllers/PassCode/View/BasePassCodeViewController.swift new file mode 100644 index 0000000..3b2c40e --- /dev/null +++ b/LeadKitAdditions/LeadKitAdditions/Controllers/PassCode/View/BasePassCodeViewController.swift @@ -0,0 +1,239 @@ +// +// Copyright (c) 2017 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import UIKit +import RxSwift +import RxCocoa +import LeadKit + +public enum PinImageType { + case entered + case clear +} + +public enum PassCodeControllerType { + case create + case enter +} + +public enum PassCodeControllerState { + case enter + case repeatEnter +} + +open class BasePassCodeViewController: UIViewController { + + public var viewModel: BasePassCodeViewModel! + + // MARK: - IBOutlets + + @IBOutlet public weak var titleLabel: UILabel? + @IBOutlet public weak var errorLabel: UILabel? + @IBOutlet public weak var dotStackView: UIStackView! + + let disposeBag = DisposeBag() + + fileprivate lazy var fakeTextField: UITextField = { + let fakeTextField = UITextField() + fakeTextField.isSecureTextEntry = true + fakeTextField.keyboardType = .numberPad + fakeTextField.isHidden = true + self.view.addSubview(fakeTextField) + return fakeTextField + }() + + // MARK: - Life circle + + override open func viewDidLoad() { + super.viewDidLoad() + + initialLoadView() + initialDotNumberConfiguration() + enebleKeyboard() + configureBackgroundNotifications() + showTouchIdIfNeeded(with: touchIdHint) + } + + // MARK: - Private functions + + private func configureBackgroundNotifications() { + guard viewModel.passCodeConfiguration.shouldResetWhenGoBackground else { + return + } + + NotificationCenter.default.rx.notification(.UIApplicationWillResignActive) + .subscribe(onNext: { [weak self] _ in + self?.resetUI() + }) + .addDisposableTo(disposeBag) + } + + private func enebleKeyboard() { + fakeTextField.becomeFirstResponder() + } + + private func initialDotNumberConfiguration() { + dotStackView.arrangedSubviews.forEach { dotStackView.removeArrangedSubview($0) } + + for _ in 0.. index, + let imageView = dotStackView.arrangedSubviews[index] as? UIImageView else { + return + } + + imageView.image = imageFor(type: state) + } + + fileprivate func setStates(for passCodeText: String) { + var statesArray: [PinImageType] = [] + + for characterIndex in 0.. UIImage { + assertionFailure("Don't use it directly. Override it!") + return UIImage() + } + + // override to change error text + func errorDescription(for error: PassCodeError) -> String { + assertionFailure("Don't use it directly. Override it!") + return "" + } + + // override to change action title text + func actionTitle(for passCodeControllerState: PassCodeControllerState) -> String { + assertionFailure("Don't use it directly. Override it!") + return "" + } + + // MARK: - Functions that can you can override to castomise your controller + + open func showError(for error: PassCodeError) { + errorLabel?.text = errorDescription(for: error) + errorLabel?.isHidden = false + } + + open func hideError() { + errorLabel?.isHidden = true + } + + // override to change UI for state + open func configureUI(for passCodeControllerState: PassCodeControllerState) { + resetDotsUI() + titleLabel?.text = actionTitle(for: passCodeControllerState) + } + +} + +// We need to implement all functions of ConfigurableController protocol to give ability to override them. +extension BasePassCodeViewController: ConfigurableController { + + open func bindViews() { + fakeTextField.rx.text.asDriver() + .do(onNext: { [weak self] text in + self?.setStates(for: text ?? "") + self?.hideError() + }) + .drive(viewModel.passCodeText) + .addDisposableTo(disposeBag) + + viewModel.validationResult + .drive(onNext: { [weak self] validationResult in + guard let validationResult = validationResult else { + return + } + + if validationResult.isValid { + self?.hideError() + } else if let pasCodeError = validationResult.error { + self?.showError(for: pasCodeError) + } + }) + .addDisposableTo(disposeBag) + + viewModel.passCodeControllerState + .drive(onNext: { [weak self] controllerState in + self?.configureUI(for: controllerState) + }) + .addDisposableTo(disposeBag) + } + + open func addViews() {} + + open func setAppearance() {} + + open func configureBarButtons() {} + + open func localize() {} + +} diff --git a/LeadKitAdditions/LeadKitAdditions/Controllers/PassCode/ViewModel/BasePassCodeViewModel.swift b/LeadKitAdditions/LeadKitAdditions/Controllers/PassCode/ViewModel/BasePassCodeViewModel.swift new file mode 100644 index 0000000..8b26e4b --- /dev/null +++ b/LeadKitAdditions/LeadKitAdditions/Controllers/PassCode/ViewModel/BasePassCodeViewModel.swift @@ -0,0 +1,172 @@ +// +// Copyright (c) 2017 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import LeadKit +import RxSwift +import RxCocoa + +public enum PassCodeAuthType { + case passCode(String) + case touchId +} + +open class BasePassCodeViewModel: BaseViewModel { + + public let controllerType: PassCodeControllerType + + public let disposeBag = DisposeBag() + + public let touchIdService: TouchIDService? + public let passCodeConfiguration: PassCodeConfiguration + + fileprivate let validationResultHolder = Variable(nil) + var validationResult: Driver { + return validationResultHolder.asDriver() + } + + fileprivate let passCodeControllerStateHolder = Variable(.enter) + public var passCodeControllerState: Driver { + return passCodeControllerStateHolder.asDriver() + } + + public let passCodeText = Variable(nil) + + fileprivate var attemptsNumber = 0 + + fileprivate lazy var passCodeHolder: PassCodeHolderProtocol = { + return PassCodeHolderBuilder.build(with: self.controllerType) + }() + + init(controllerType: PassCodeControllerType, + passCodeConfiguration: PassCodeConfiguration, + touchIdService: TouchIDService? = nil) { + + self.controllerType = controllerType + self.passCodeConfiguration = passCodeConfiguration + self.touchIdService = touchIdService + + bindViewModel() + } + + private func bindViewModel() { + passCodeText.asDriver() + .distinctUntilChanged { $0 == $1 } + .drive(onNext: { [weak self] passCode in + if let passCode = passCode, + passCode.characters.count == Int(self?.passCodeConfiguration.passCodeCharactersNumber ?? 0) { + self?.set(passCode: passCode) + } + }) + .addDisposableTo(disposeBag) + + validationResultHolder.asDriver() + .drive(onNext: { [weak self] validationResult in + if validationResult?.isValid ?? false, let passCode = validationResult?.passCode { + self?.authSucceed(.passCode(passCode)) + } else { + self?.passCodeControllerStateHolder.value = .enter + } + }) + .addDisposableTo(disposeBag) + } + + public func reset() { + passCodeText.value = nil + validationResultHolder.value = nil + passCodeControllerStateHolder.value = .enter + attemptsNumber = 0 + passCodeHolder.reset() + } + + // MARK: - HAVE TO OVERRIDE + + open func isEnteredPassCodeValid(_ passCode: String) -> Bool { + assertionFailure("Don't use it directly. Override it!") + return false + } + + open func authSucceed(_ type: PassCodeAuthType) { + assertionFailure("Don't use it directly. Override it!") + } + + // MARK: - Functions that can you can override to use TouchId + + open var isTouchIdEnabled: Bool { + return false + } + + open func activateTouchIdForUser() { + assertionFailure("Don't use it directly. Override it!") + } + +} + +extension BasePassCodeViewModel { + + fileprivate func set(passCode: String) { + passCodeHolder.add(passCode: passCode) + validateIfNeeded() + + if shouldUpdateControllerState { + switch passCodeHolder.enterStep { + case .first: + passCodeControllerStateHolder.value = .enter + case .second: + passCodeControllerStateHolder.value = .repeatEnter + } + } + } + + private var shouldUpdateControllerState: Bool { + return !passCodeHolder.shouldValidate || + !(validationResultHolder.value?.isValid ?? true) || + validationResultHolder.value?.error == .tooMuchAttempts + } + + private func validateIfNeeded() { + guard passCodeHolder.shouldValidate else { + return + } + + var validationResult = passCodeHolder.validate() + + if passCodeHolder.type == .enter { + attemptsNumber += 1 + + if let passCode = validationResult.passCode, !isEnteredPassCodeValid(passCode) { + validationResult = .inValid(.wrongCode) + } + + if (!validationResult.isValid && attemptsNumber == Int(passCodeConfiguration.maxAttemptsLoginNumber)) || + attemptsNumber > Int(passCodeConfiguration.maxAttemptsLoginNumber) { + validationResult = .inValid(.tooMuchAttempts) + } + } + + if !validationResult.isValid { + passCodeHolder.reset() + } + + validationResultHolder.value = validationResult + } + +}