diff --git a/CHANGELOG.md b/CHANGELOG.md
index e601bb15..ece54f98 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
# Changelog
+### 0.9.41
+- **Add**: `OTPSwiftView` - a fully customizable OTP view.
+- **Add**: `BaseInitializableControl` UIControl conformance to InitializableView.
+- **Add**: `TISwiftUtils` a bunch of useful helpers for development.
+
### 0.9.40
- **Fix**: Load more request repetion in `PaginationWrapper`.
diff --git a/LeadKit.podspec b/LeadKit.podspec
index 67cee149..b1c1170d 100644
--- a/LeadKit.podspec
+++ b/LeadKit.podspec
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "LeadKit"
- s.version = "0.9.40"
+ s.version = "0.9.41"
s.summary = "iOS framework with a bunch of tools for rapid development"
s.homepage = "https://github.com/TouchInstinct/LeadKit"
s.license = "Apache License, Version 2.0"
diff --git a/OTPSwiftView/Assets/preview.gif b/OTPSwiftView/Assets/preview.gif
new file mode 100644
index 00000000..7738c77b
Binary files /dev/null and b/OTPSwiftView/Assets/preview.gif differ
diff --git a/OTPSwiftView/README.md b/OTPSwiftView/README.md
new file mode 100644
index 00000000..61585f8b
--- /dev/null
+++ b/OTPSwiftView/README.md
@@ -0,0 +1,151 @@
+# OTPSwiftView
+
+
+
+A fully customizable OTP view.
+
+
+
+
+
+# Usage
+```swift
+class ViewController: UIViewController {
+ let otpView = CustomOTPSwiftView() // Custom OTP view
+
+ let config = OTPCodeConfig(codeSymbolsCount: 6, // Base configuration of OTP view
+ spacing: 6,
+ customSpacing: [2: 20])
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ /*
+ Add your codeView and set layout
+ */
+
+ /* Configure OTP view */
+
+ otpView.configure(with: config)
+
+ /* Bind events */
+
+ otpView.onTextEnter = { code in
+ // Get code from codeView
+ }
+
+ /* Update text */
+
+ otpView.code = "234435"
+
+ /* Update focus */
+
+ otpView.beginFirstResponder() // show keyboard
+ otpView.resignFirstResponder() // hide keyboard
+ }
+}
+```
+
+# Customization
+## Single OTP View
+*OTPView* is a base class that describes a single OTP textfield.
+To customize the appearance and layout, you must inherit from the OTPView.
+*Don't forget to add UIGestureRecognizer to call closure `onTap?()`. Use UITapGestureRecognizer to avoid bugs.*
+
+```swift
+import OTPSwiftView
+
+class CustomOTPView: OTPView {
+ override func addViews() {
+ super.addViews()
+
+ // Adding additional views to current view. The OTP textfield has already been added.
+ }
+
+ override func configureLayout() {
+ super.configureLayout()
+
+ // Confgiure layout of subviews
+ }
+
+ override func bindViews() {
+ super.bindViews()
+
+ // Binding to data or user actions
+
+ let gesture = UITapGestureRecognizer(target: self, action: #selector(onTapAction))
+ addGestureRecognizer(gesture)
+ }
+
+ private func onTapAction() {
+ onTap?()
+ }
+
+ override func configureAppearance() {
+ super.configureAppearance()
+
+ // Appearance configuration method
+ }
+}
+```
+
+*If needed to set validation for input use `validationClosure: ValidationClosure?`*. For example, only numbers validation:
+
+```swift
+import OTPSwiftView
+
+class CustomOTPView: OTPView {
+
+ override func bindViews() {
+ super.bindViews()
+
+ codeTextField.validationClosure = { input in
+ input.allSatisfy { $0.isNumber }
+ }
+ }
+}
+```
+
+## OTPSwiftView
+*OTPSwiftView* is a base class that is responsible for the layout of single OTP views.
+As with OTPView, you should create an heir class to configure your full OTP view.
+
+```swift
+import OTPSwiftView
+
+final class CustomOTPSwiftView: OTPSwiftView {
+ override func addViews() {
+ super.addViews()
+
+ // Adding additional views to current code view. The single OTP views has already been added.
+ }
+
+ override func configureLayout() {
+ super.configureLayout()
+
+ // Confgiure layout of subviews
+ }
+
+ override func bindViews() {
+ super.bindViews()
+
+ // Binding to data or user actions
+ }
+
+ override func configureAppearance() {
+ super.configureAppearance()
+
+ // Appearance configuration method
+ }
+
+ override func configure(with config: OTPCodeConfig) {
+ super.configure(with: config)
+
+ // Configure you code view with configuration
+ }
+}
+```
+
+# Installation via SPM
+
+You can install this framework as a target of LeadKit.
diff --git a/OTPSwiftView/Sources/Models/OTPCodeConfig.swift b/OTPSwiftView/Sources/Models/OTPCodeConfig.swift
new file mode 100644
index 00000000..3fa0ae67
--- /dev/null
+++ b/OTPSwiftView/Sources/Models/OTPCodeConfig.swift
@@ -0,0 +1,38 @@
+//
+// 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
+
+/// Base configuration for OTPSwiftView
+open class OTPCodeConfig {
+ public typealias Spacing = [Int: CGFloat]
+
+ public let codeSymbolsCount: Int
+ public let spacing: CGFloat
+ public let customSpacing: Spacing?
+
+ public init(codeSymbolsCount: Int, spacing: CGFloat, customSpacing: Spacing?) {
+ self.codeSymbolsCount = codeSymbolsCount
+ self.spacing = spacing
+ self.customSpacing = customSpacing
+ }
+}
diff --git a/OTPSwiftView/Sources/Views/OTPSwiftView/OTPSwiftView.swift b/OTPSwiftView/Sources/Views/OTPSwiftView/OTPSwiftView.swift
new file mode 100644
index 00000000..1df6a0b1
--- /dev/null
+++ b/OTPSwiftView/Sources/Views/OTPSwiftView/OTPSwiftView.swift
@@ -0,0 +1,149 @@
+//
+// 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 TIUIKitCore
+import TISwiftUtils
+
+/// Base full OTP View for entering the verification code
+open class OTPSwiftView: 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?
+
+ 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..?
+ public var caretHeight: CGFloat?
+
+ public var lastNotEmpty: OTPTextField {
+ let isLastNotEmpty = !text.orEmpty.isEmpty && nextTextField?.text.orEmpty.isEmpty ?? true
+ return isLastNotEmpty ? self : nextTextField?.lastNotEmpty ?? self
+ }
+
+ open override var font: UIFont? {
+ didSet {
+ if caretHeight == nil, let font = font {
+ caretHeight = font.pointSize - font.descender
+ }
+ }
+ }
+
+ public override init(frame: CGRect) {
+ super.init(frame: frame)
+
+ delegate = self
+ }
+
+ @available(*, unavailable)
+ required public init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ open override func deleteBackward() {
+ guard text.orEmpty.isEmpty else {
+ return
+ }
+
+ onTextChangedSignal?()
+ previousTextField?.text = ""
+ previousTextField?.becomeFirstResponder()
+ }
+
+ public func set(inputText: String) {
+ text = inputText.prefix(maxSymbolsCount).string
+
+ let nextInputText = inputText.count >= maxSymbolsCount
+ ? inputText.suffix(inputText.count - maxSymbolsCount).string
+ : ""
+
+ nextTextField?.set(inputText: nextInputText)
+ }
+
+ open override func caretRect(for position: UITextPosition) -> CGRect {
+ guard let caretHeight = caretHeight else {
+ return super.caretRect(for: position)
+ }
+
+ var superRect = super.caretRect(for: position)
+ superRect.size.height = caretHeight
+
+ return superRect
+ }
+
+ open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
+ let view = super.hitTest(point, with: event)
+ return view == self && isFirstResponder ? view : nil
+ }
+}
+
+extension OTPTextField: UITextFieldDelegate {
+ public func textField(_ textField: UITextField,
+ shouldChangeCharactersIn range: NSRange,
+ replacementString string: String) -> Bool {
+ guard let textField = textField as? OTPTextField else {
+ return true
+ }
+
+ let isInputEmpty = textField.text.orEmpty.isEmpty && string.isEmpty
+
+ guard isInputEmpty || validationClosure?(string) ?? true else {
+ return false
+ }
+
+ switch range.length {
+ case 0: // set text to textfield
+ textField.set(inputText: string)
+
+ let currentTextField = textField.lastNotEmpty.nextTextField ?? textField.lastNotEmpty
+ currentTextField.becomeFirstResponder()
+ textField.onTextChangedSignal?()
+
+ return false
+
+ case 1: // remove character from textfield
+ textField.text = ""
+ textField.onTextChangedSignal?()
+ return false
+
+ default:
+ return true
+ }
+ }
+}
diff --git a/OTPSwiftView/Sources/Views/OTPView/OTPView.swift b/OTPSwiftView/Sources/Views/OTPView/OTPView.swift
new file mode 100644
index 00000000..c2a29468
--- /dev/null
+++ b/OTPSwiftView/Sources/Views/OTPView/OTPView.swift
@@ -0,0 +1,37 @@
+//
+// 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 TIUIKitCore
+import TISwiftUtils
+
+/// Base OTP view with textfield for entering a one symbol
+open class OTPView: BaseInitializableView {
+ public let codeTextField = OTPTextField()
+
+ public var onTap: VoidClosure?
+
+ open override func addViews() {
+ super.addViews()
+
+ addSubview(codeTextField)
+ }
+}
diff --git a/Package.swift b/Package.swift
index 6d1622df..374b919e 100644
--- a/Package.swift
+++ b/Package.swift
@@ -9,11 +9,15 @@ let package = Package(
products: [
.library(name: "TITransitions", targets: ["TITransitions"]),
.library(name: "TIUIKitCore", targets: ["TIUIKitCore"]),
- .library(name: "TIUIElements", targets: ["TIUIElements"])
+ .library(name: "TISwiftUtils", targets: ["TISwiftUtils"]),
+ .library(name: "TIUIElements", targets: ["TIUIElements"]),
+ .library(name: "OTPSwiftView", targets: ["OTPSwiftView"])
],
targets: [
.target(name: "TITransitions", path: "TITransitions/Sources"),
.target(name: "TIUIKitCore", path: "TIUIKitCore/Sources"),
- .target(name: "TIUIElements", dependencies: ["TIUIKitCore"], path: "TIUIElements/Sources")
+ .target(name: "TISwiftUtils", path: "TISwiftUtils/Sources"),
+ .target(name: "TIUIElements", dependencies: ["TIUIKitCore"], path: "TIUIElements/Sources"),
+ .target(name: "OTPSwiftView", dependencies: ["TIUIKitCore"], path: "OTPSwiftView/Sources")
]
)
diff --git a/README.md b/README.md
index b0849610..6cda623b 100644
--- a/README.md
+++ b/README.md
@@ -6,4 +6,6 @@ This repository contains the following additional frameworks:
- [TIUIKitCore](TIUIKitCore) - core ui elements and protocols from LeadKit.
- [TITransitions](TITransitions) - set of custom transitions to present controller.
- [TIUIElements](TIUIElements) - bunch of of useful protocols and views.
+- [OTPSwiftView](OTPSwiftView) - a fully customizable OTP view.
+- [TISwiftUtils](TISwiftUtils) - a bunch of useful helpers for development.
diff --git a/TISwiftUtils/README.md b/TISwiftUtils/README.md
new file mode 100644
index 00000000..d135d3e7
--- /dev/null
+++ b/TISwiftUtils/README.md
@@ -0,0 +1,7 @@
+# TISwiftUtils
+
+Bunch of useful helpers for development.
+
+# Installation via SPM
+
+You can install this framework as a target of LeadKit.
diff --git a/TISwiftUtils/Sources/Extensions/Optional/Optional+Extensions.swift b/TISwiftUtils/Sources/Extensions/Optional/Optional+Extensions.swift
new file mode 100644
index 00000000..0bddd3bb
--- /dev/null
+++ b/TISwiftUtils/Sources/Extensions/Optional/Optional+Extensions.swift
@@ -0,0 +1,29 @@
+//
+// 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
+
+public extension Optional where Wrapped == String {
+ var orEmpty: String {
+ self ?? ""
+ }
+}
diff --git a/TISwiftUtils/Sources/Extensions/Substring/Substring+Extensions.swift b/TISwiftUtils/Sources/Extensions/Substring/Substring+Extensions.swift
new file mode 100644
index 00000000..61d0812f
--- /dev/null
+++ b/TISwiftUtils/Sources/Extensions/Substring/Substring+Extensions.swift
@@ -0,0 +1,30 @@
+//
+// 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 Foundation
+
+public extension Substring {
+ var string: String {
+ String(self)
+ }
+}
+
diff --git a/TISwiftUtils/Sources/Helpers/Typealias.swift b/TISwiftUtils/Sources/Helpers/Typealias.swift
new file mode 100644
index 00000000..897d1f09
--- /dev/null
+++ b/TISwiftUtils/Sources/Helpers/Typealias.swift
@@ -0,0 +1,51 @@
+//
+// 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
+
+/// Closure with custom arguments and return value.
+public typealias Closure = (Input) -> Output
+
+/// Closure with no arguments and custom return value.
+public typealias ResultClosure