LeadKit/OTPSwiftView/Sources/Views/OTPSwiftView/OTPSwiftView.swift

150 lines
5.1 KiB
Swift

//
// Copyright (c) 2020 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 TIUIElements
import TISwiftUtils
/// Base full OTP View for entering the verification code
open class OTPSwiftView<View: OTPView>: BaseInitializableControl {
private var emptyOTPView: View? {
textFieldsCollection.first { $0.codeTextField.text.orEmpty.isEmpty } ?? textFieldsCollection.last
}
public private(set) var codeStackView = UIStackView()
public private(set) var textFieldsCollection: [View] = []
public var onTextEnter: ParameterClosure<String>?
public var code: String {
get {
textFieldsCollection.compactMap { $0.codeTextField.text }.joined()
}
set {
textFieldsCollection.first?.codeTextField.set(inputText: newValue)
}
}
public override var isFirstResponder: Bool {
!textFieldsCollection.allSatisfy { !$0.codeTextField.isFirstResponder }
}
open override func addViews() {
super.addViews()
addSubview(codeStackView)
}
open override func configureAppearance() {
super.configureAppearance()
codeStackView.contentMode = .center
codeStackView.distribution = .fillEqually
}
open func configure(with config: OTPCodeConfig) {
textFieldsCollection = createTextFields(numberOfFields: config.codeSymbolsCount)
codeStackView.addArrangedSubviews(textFieldsCollection)
codeStackView.spacing = config.spacing
configure(customSpacing: config.customSpacing, for: codeStackView)
bindTextFields(with: config)
}
@discardableResult
open override func becomeFirstResponder() -> Bool {
guard let emptyOTPView = emptyOTPView, !emptyOTPView.isFirstResponder else {
return false
}
return emptyOTPView.codeTextField.becomeFirstResponder()
}
@discardableResult
open override func resignFirstResponder() -> Bool {
guard let emptyOTPView = emptyOTPView, emptyOTPView.isFirstResponder else {
return false
}
return emptyOTPView.codeTextField.resignFirstResponder()
}
}
// MARK: - Configure textfields
private extension OTPSwiftView {
func configure(customSpacing: OTPCodeConfig.Spacing?, for stackView: UIStackView) {
guard let customSpacing = customSpacing else {
return
}
customSpacing.forEach { viewIndex, spacing in
guard viewIndex < stackView.arrangedSubviews.count, viewIndex >= .zero else {
return
}
self.set(spacing: spacing,
after: stackView.arrangedSubviews[viewIndex],
for: stackView)
}
}
func set(spacing: CGFloat, after view: UIView, for stackView: UIStackView) {
stackView.setCustomSpacing(spacing, after: view)
}
func createTextFields(numberOfFields: Int) -> [View] {
var textFieldsCollection: [View] = []
(.zero..<numberOfFields).forEach { _ in
let textField = View()
textField.codeTextField.previousTextField = textFieldsCollection.last?.codeTextField
textFieldsCollection.last?.codeTextField.nextTextField = textField.codeTextField
textFieldsCollection.append(textField)
}
return textFieldsCollection
}
func bindTextFields(with config: OTPCodeConfig) {
let onTextChangedSignal: VoidClosure = { [weak self] in
guard let code = self?.code else {
return
}
let correctedCode = code.prefix(config.codeSymbolsCount).string
self?.onTextEnter?(correctedCode)
}
let onTap: VoidClosure = { [weak self] in
self?.becomeFirstResponder()
}
textFieldsCollection.forEach {
$0.codeTextField.onTextChangedSignal = onTextChangedSignal
$0.onTap = onTap
}
}
}