Merge remote-tracking branch 'origin/master' into feature/podspecs_update

This commit is contained in:
krasich74 2020-08-31 09:30:36 +03:00
commit d595e0331f
57 changed files with 2440 additions and 142 deletions

View File

@ -1,5 +1,59 @@
# Changelog
### 0.9.43
- **Fix**: `OTPSwiftView`'s dependencies.
### 0.9.42
- **Fix**: Logic bugs of `PaginationWrapper`.
### 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`.
### 0.9.39
- **Add**: `Animatable` protocol to TIUIKitCore.
- **Add**: `ActivityIndicator` protocol to TIUIKitCore.
- **Add**: `ActivityIndicatorHolder` protocol to TIUIKitCore.
- **Add**: `TIUIElements` for ui elements.
### 0.9.38
- **Add**: `BaseRxTableViewCell` is subclass of `UITableViewCell` class with support `InitializableView` and `DisposeBagHolder` protocols.
- **Add**: `ContainerTableCell` is container class that provides wrapping any `UIView` into `UITableViewCell`.
- **Add**: `BaseTappableViewModel` is simplifies interaction between view and viewModel for events of tapping.
- **Add**: `VoidTappableViewModel` is subclass of `BaseTappableViewModel` class with void payload type.
### 0.9.37
- **Fix**: ScrollView content offset of `PaginationWrapper` for iOS 13.
- **Fix**: Load more request crash of `PaginationWrapper`.
### 0.9.36
- **Add**: SPM Package.swift.
- **Add**: TITransitions via SPM.
- **Add**: TIUIKitCore via SPM.
- **Update**: Readme.
### 0.9.35
- **Add**: Selector `refreshAction()` for refresh control of `PaginationWrapper`.
### 0.9.34
- **Add**: `ButtonHolder` - protocol that contains button property.
- **Add**: `ButtonHolderView` - view which contains button.
- **Add**: Conformance `UIButton` to `ButtonHolder`.
- **Add**: Conformance `BasePlaceholderView` to `ButtonHolderView`.
- **[Breaking change]**: Replace functions `footerRetryButton() -> UIButton?` to `footerRetryView() -> ButtonHolderView?` and `footerRetryButtonHeight() -> CGFloat` to `footerRetryViewHeight() -> CGFloat` for `PaginationWrapperUIDelegate`.
- **[Breaking change]**: Replace functions `footerRetryButtonWillAppear()` to `footerRetryViewWillAppear()` and `footerRetryButtonWillDisappear()` to `footerRetryViewWillDisappear()` for `PaginationWrapperUIDelegate`.
### 0.9.33
- **Fix**: `CustomizableButtonView` container class that provides great customization.
- **Fix**: `CustomizableButtonViewModel` viewModel class for `CustomizableButtonView` configuration.
### 0.9.32
- **Fix**: `CustomizableButtonView` container class that provides great customization.
### 0.9.31
- **Add**: `@discardableResult` to function - `replace(with:at:with:manualBeginEndUpdates)` in `TableDirector`.

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "LeadKit"
s.version = "0.9.32"
s.version = "0.9.43"
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"

View File

@ -16,6 +16,13 @@
40F118471F8FEF97004AADAF /* AppearanceConfigurable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40F118461F8FEF97004AADAF /* AppearanceConfigurable.swift */; };
40F118491F8FF223004AADAF /* TableRow+AppearanceExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40F118481F8FF223004AADAF /* TableRow+AppearanceExtension.swift */; };
411073AF23466B41002DD9B9 /* UIViewController+PresentFullScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 411073AE23466B41002DD9B9 /* UIViewController+PresentFullScreen.swift */; };
4CF65D1424DD684A0006B001 /* ButtonHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF65D1324DD684A0006B001 /* ButtonHolder.swift */; };
4CF65D1624DD69250006B001 /* UIButton+ButtonHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF65D1524DD69250006B001 /* UIButton+ButtonHolder.swift */; };
4CF65D1824DD6C080006B001 /* ButtonHolderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF65D1724DD6C080006B001 /* ButtonHolderView.swift */; };
52421F8D24EAB52E00948DD1 /* ContainerTableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52421F8C24EAB52E00948DD1 /* ContainerTableCell.swift */; };
52421F8F24EAB84900948DD1 /* BaseRxTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52421F8E24EAB84900948DD1 /* BaseRxTableViewCell.swift */; };
52421F9424EBCFAE00948DD1 /* VoidTappableViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52421F9324EBCFAE00948DD1 /* VoidTappableViewModel.swift */; };
52421F9624EBCFBB00948DD1 /* BaseTappableViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52421F9524EBCFBB00948DD1 /* BaseTappableViewModel.swift */; };
67051ADB1EBC7C36008EADC0 /* SpinnerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67051ADA1EBC7C36008EADC0 /* SpinnerView.swift */; };
67051ADD1EBC7C36008EADC0 /* SpinnerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67051ADA1EBC7C36008EADC0 /* SpinnerView.swift */; };
6713C23720AF0C4D00875921 /* NetworkOperationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6713C23620AF0C4D00875921 /* NetworkOperationState.swift */; };
@ -550,6 +557,13 @@
40F118461F8FEF97004AADAF /* AppearanceConfigurable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceConfigurable.swift; sourceTree = "<group>"; };
40F118481F8FF223004AADAF /* TableRow+AppearanceExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TableRow+AppearanceExtension.swift"; sourceTree = "<group>"; };
411073AE23466B41002DD9B9 /* UIViewController+PresentFullScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+PresentFullScreen.swift"; sourceTree = "<group>"; };
4CF65D1324DD684A0006B001 /* ButtonHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonHolder.swift; sourceTree = "<group>"; };
4CF65D1524DD69250006B001 /* UIButton+ButtonHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIButton+ButtonHolder.swift"; sourceTree = "<group>"; };
4CF65D1724DD6C080006B001 /* ButtonHolderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonHolderView.swift; sourceTree = "<group>"; };
52421F8C24EAB52E00948DD1 /* ContainerTableCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContainerTableCell.swift; sourceTree = "<group>"; };
52421F8E24EAB84900948DD1 /* BaseRxTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseRxTableViewCell.swift; sourceTree = "<group>"; };
52421F9324EBCFAE00948DD1 /* VoidTappableViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoidTappableViewModel.swift; sourceTree = "<group>"; };
52421F9524EBCFBB00948DD1 /* BaseTappableViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseTappableViewModel.swift; sourceTree = "<group>"; };
67051ADA1EBC7C36008EADC0 /* SpinnerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpinnerView.swift; sourceTree = "<group>"; };
6713C23620AF0C4D00875921 /* NetworkOperationState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkOperationState.swift; sourceTree = "<group>"; };
6713C23B20AF0D5900875921 /* NetworkOperationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkOperationModel.swift; sourceTree = "<group>"; };
@ -859,6 +873,48 @@
path = UITableView;
sourceTree = "<group>";
};
4CF65D1924DD6C3D0006B001 /* ButtonHolder */ = {
isa = PBXGroup;
children = (
4CF65D1324DD684A0006B001 /* ButtonHolder.swift */,
4CF65D1724DD6C080006B001 /* ButtonHolderView.swift */,
);
path = ButtonHolder;
sourceTree = "<group>";
};
52421F8B24EAB52E00948DD1 /* ContainerTableCell */ = {
isa = PBXGroup;
children = (
52421F8C24EAB52E00948DD1 /* ContainerTableCell.swift */,
);
path = ContainerTableCell;
sourceTree = "<group>";
};
52421F9024EAB84E00948DD1 /* BaseRxTableViewCell */ = {
isa = PBXGroup;
children = (
52421F8E24EAB84900948DD1 /* BaseRxTableViewCell.swift */,
);
path = BaseRxTableViewCell;
sourceTree = "<group>";
};
52421F9124EBCF6E00948DD1 /* ViewModels */ = {
isa = PBXGroup;
children = (
52421F9224EBCF8600948DD1 /* TappableViewModel */,
);
path = ViewModels;
sourceTree = "<group>";
};
52421F9224EBCF8600948DD1 /* TappableViewModel */ = {
isa = PBXGroup;
children = (
52421F9524EBCFBB00948DD1 /* BaseTappableViewModel.swift */,
52421F9324EBCFAE00948DD1 /* VoidTappableViewModel.swift */,
);
path = TappableViewModel;
sourceTree = "<group>";
};
671461C41EB3396E00EAB194 /* Classes */ = {
isa = PBXGroup;
children = (
@ -867,6 +923,7 @@
6774527E2062566D0024EEEF /* DataLoading */,
671461D21EB3396E00EAB194 /* Services */,
671461D41EB3396E00EAB194 /* Views */,
52421F9124EBCF6E00948DD1 /* ViewModels */,
);
path = Classes;
sourceTree = "<group>";
@ -904,6 +961,8 @@
671461D41EB3396E00EAB194 /* Views */ = {
isa = PBXGroup;
children = (
52421F9024EAB84E00948DD1 /* BaseRxTableViewCell */,
52421F8B24EAB52E00948DD1 /* ContainerTableCell */,
72005A1A2266226800ECE090 /* CustomizableButton */,
677B06B6211873E7006C947D /* BasePlaceholderView */,
67DB77672108714A001CB56B /* CollectionViewWrapperView */,
@ -1433,6 +1492,7 @@
6741CE9F20E2413300FEC4D9 /* UIKit */ = {
isa = PBXGroup;
children = (
4CF65D1924DD6C3D0006B001 /* ButtonHolder */,
6741CEA020E2416C00FEC4D9 /* ScrollViewHolder.swift */,
6741CEA420E2418200FEC4D9 /* TableViewHolder.swift */,
6741CEA820E2418B00FEC4D9 /* CollectionViewHolder.swift */,
@ -1845,6 +1905,7 @@
isa = PBXGroup;
children = (
67E352512119AC060035BDDB /* UIButton+ViewTextConfigurable.swift */,
4CF65D1524DD69250006B001 /* UIButton+ButtonHolder.swift */,
);
path = UIButton;
sourceTree = "<group>";
@ -2395,8 +2456,10 @@
678D26A420692BFF00B05B93 /* TextFieldViewModelEvents.swift in Sources */,
671462801EB3396E00EAB194 /* DataRequest+Extensions.swift in Sources */,
67EB7FF8206175F700BDD9FB /* PaginationWrappable.swift in Sources */,
52421F9624EBCFBB00948DD1 /* BaseTappableViewModel.swift in Sources */,
67990AD6213EA6A50040D195 /* ContentLoadingViewModel+Extensions.swift in Sources */,
671463541EB3396E00EAB194 /* StaticViewHeightProtocol.swift in Sources */,
4CF65D1824DD6C080006B001 /* ButtonHolderView.swift in Sources */,
72AECC6B224A979D00D12E7C /* BaseSearchViewController.swift in Sources */,
673CF4112063ABD100C329F6 /* GeneralDataLoadingState+Extensions.swift in Sources */,
72005A1E2266226800ECE090 /* CustomizableButton.swift in Sources */,
@ -2424,6 +2487,7 @@
671462FC1EB3396E00EAB194 /* UIView+XibNameProtocol.swift in Sources */,
67EB7FC0206140E600BDD9FB /* TotalCountCursor.swift in Sources */,
36DAAF512007CC920090BE0D /* UITableView+Extensions.swift in Sources */,
4CF65D1624DD69250006B001 /* UIButton+ButtonHolder.swift in Sources */,
671463841EB3396E00EAB194 /* ResizeDrawingOperation.swift in Sources */,
6774528D20625C9E0024EEEF /* GeneralDataLoadingState.swift in Sources */,
72005A1F2266226800ECE090 /* CustomizableButtonViewModel.swift in Sources */,
@ -2500,6 +2564,7 @@
A6E0DDF11F8A6C80002CA74E /* SeparatorConfiguration.swift in Sources */,
6727477F206CD3BD00725163 /* ViewText+Extensions.swift in Sources */,
67EB7FEB2061667900BDD9FB /* DefaultTotalCountCursorListingResult.swift in Sources */,
4CF65D1424DD684A0006B001 /* ButtonHolder.swift in Sources */,
671AD26C206A3E8500EAF887 /* Array+TotalCountCursorListingResult.swift in Sources */,
673CF4382063E7CE00C329F6 /* GeneralDataLoadingController+DefaultImplementation.swift in Sources */,
B85B768720B1CF6700F837C4 /* Encodable+Extensions.swift in Sources */,
@ -2537,8 +2602,10 @@
411073AF23466B41002DD9B9 /* UIViewController+PresentFullScreen.swift in Sources */,
671462941EB3396E00EAB194 /* CGSize+CGContextSize.swift in Sources */,
6741CEA920E2418B00FEC4D9 /* CollectionViewHolder.swift in Sources */,
52421F9424EBCFAE00948DD1 /* VoidTappableViewModel.swift in Sources */,
67745279206252020024EEEF /* DataLoadingState.swift in Sources */,
671463641EB3396E00EAB194 /* ViewHeightProtocol.swift in Sources */,
52421F8F24EAB84900948DD1 /* BaseRxTableViewCell.swift in Sources */,
67EB7FDA20615D5B00BDD9FB /* ResettableRxCursorDataSource.swift in Sources */,
671462481EB3396E00EAB194 /* FixedPageCursor.swift in Sources */,
671462C81EB3396E00EAB194 /* String+Localization.swift in Sources */,
@ -2586,6 +2653,7 @@
678D267920691D8200B05B93 /* DataModelFieldBinding.swift in Sources */,
72AECC71224A97F100D12E7C /* SearchResultsViewController.swift in Sources */,
673CF4342063E29B00C329F6 /* TextWithButtonPlaceholder.swift in Sources */,
52421F8D24EAB52E00948DD1 /* ContainerTableCell.swift in Sources */,
673CF4222063D90600C329F6 /* DisposeBagHolder.swift in Sources */,
67DB776D210871E8001CB56B /* BaseCollectionContentController.swift in Sources */,
82B4F8DB223903B800F6708C /* Block.swift in Sources */,

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

151
OTPSwiftView/README.md Normal file
View File

@ -0,0 +1,151 @@
# OTPSwiftView
![Platform](https://img.shields.io/badge/platform-iOS-green)
A fully customizable OTP view.
<p align="left">
<img src="Assets/preview.gif" width=300 height=533>
</p>
# 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<String>?`*. 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<CustomOTPView> {
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.

View File

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

View File

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

View File

@ -0,0 +1,131 @@
//
// 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 TISwiftUtils
/// Base one symbol textfield
open class OTPTextField: UITextField {
private let maxSymbolsCount = 1
public weak var previousTextField: OTPTextField?
public weak var nextTextField: OTPTextField?
public var onTextChangedSignal: VoidClosure?
public var validationClosure: Closure<String, Bool>?
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
}
}
}

View File

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

23
Package.swift Normal file
View File

@ -0,0 +1,23 @@
// swift-tools-version:5.0
import PackageDescription
let package = Package(
name: "LeadKit",
platforms: [
.iOS(.v11)
],
products: [
.library(name: "TITransitions", targets: ["TITransitions"]),
.library(name: "TIUIKitCore", targets: ["TIUIKitCore"]),
.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: "TISwiftUtils", path: "TISwiftUtils/Sources"),
.target(name: "TIUIElements", dependencies: ["TIUIKitCore"], path: "TIUIElements/Sources"),
.target(name: "OTPSwiftView", dependencies: ["TIUIKitCore", "TISwiftUtils"], path: "OTPSwiftView/Sources")
]
)

View File

@ -1,2 +1,11 @@
# LeadKit
LeadKit it's a iOS framework with a bunch of tools for rapid app development
LeadKit is the iOS framework with a bunch of tools for rapid app development.
## Additional
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.

View File

@ -60,13 +60,15 @@ final public class PaginationWrapper<Cursor: ResettableRxDataSourceCursor, Deleg
}
}
private var bottom: CGFloat {
wrappedView.scrollView.contentSize.height - wrappedView.scrollView.frame.size.height
}
private let disposeBag = DisposeBag()
private var currentPlaceholderView: UIView?
private var currentPlaceholderViewTopConstraint: NSLayoutConstraint?
private let applicationCurrentyActive = BehaviorRelay(value: true)
/// Initializer with table view, placeholders container view, cusor and delegate parameters.
///
/// - Parameters:
@ -87,8 +89,6 @@ final public class PaginationWrapper<Cursor: ResettableRxDataSourceCursor, Deleg
bindViewModelStates()
createRefreshControl()
bindAppStateNotifications()
}
/// Method that reload all data in internal view model.
@ -121,7 +121,7 @@ final public class PaginationWrapper<Cursor: ResettableRxDataSourceCursor, Deleg
if case .initial = afterState {
wrappedView.scrollView.isUserInteractionEnabled = false
removeCurrentPlaceholderView()
removeAllPlaceholderView()
guard let loadingIndicator = uiDelegate?.initialLoadingIndicator() else {
return
@ -144,7 +144,7 @@ final public class PaginationWrapper<Cursor: ResettableRxDataSourceCursor, Deleg
private func onLoadingMoreState(afterState: LoadingState) {
if case .error = afterState { // user tap retry button in table footer
uiDelegate?.footerRetryButtonWillDisappear()
uiDelegate?.footerRetryViewWillDisappear()
wrappedView.footerView = nil
addInfiniteScroll(withHandler: false)
wrappedView.scrollView.beginInfiniteScroll(true)
@ -160,7 +160,7 @@ final public class PaginationWrapper<Cursor: ResettableRxDataSourceCursor, Deleg
if case .initialLoading = afterState {
delegate?.paginationWrapper(didReload: newItems, using: cursor)
removeCurrentPlaceholderView()
removeAllPlaceholderView()
wrappedView.scrollView.support.refreshControl?.endRefreshing()
@ -168,7 +168,8 @@ final public class PaginationWrapper<Cursor: ResettableRxDataSourceCursor, Deleg
} else if case .loadingMore = afterState {
delegate?.paginationWrapper(didLoad: newItems, using: cursor)
readdInfiniteScrollWithHandler()
removeAllPlaceholderView()
addInfiniteScrollWithHandler()
}
}
@ -186,35 +187,39 @@ final public class PaginationWrapper<Cursor: ResettableRxDataSourceCursor, Deleg
}
replacePlaceholderViewIfNeeded(with: errorView)
} else if case .loadingMore = afterState {
guard let retryButton = uiDelegate?.footerRetryButton(),
let retryButtonHeight = uiDelegate?.footerRetryButtonHeight() else {
} else {
guard let retryView = uiDelegate?.footerRetryView(),
let retryViewHeight = uiDelegate?.footerRetryViewHeight() else {
removeInfiniteScroll()
return
}
retryButton.frame = CGRect(x: 0, y: 0, width: wrappedView.scrollView.bounds.width, height: retryButtonHeight)
retryView.frame = CGRect(x: 0, y: 0, width: wrappedView.scrollView.bounds.width, height: retryViewHeight)
retryView.button.addTarget(self, action: #selector(retryEvent), for: .touchUpInside)
retryButton.rx
.controlEvent(.touchUpInside)
.asDriver()
.drive(retryEvent)
.disposed(by: disposeBag)
uiDelegate?.footerRetryButtonWillAppear()
uiDelegate?.footerRetryViewWillAppear()
removeInfiniteScroll { scrollView in
self.wrappedView.footerView = retryButton
self.wrappedView.footerView = retryView
let newContentOffset = CGPoint(x: 0, y: scrollView.contentOffset.y + retryButtonHeight)
let shouldUpdateContentOffset = Int(scrollView.contentOffset.y + retryViewHeight) >= Int(self.bottom)
scrollView.setContentOffset(newContentOffset, animated: true)
if shouldUpdateContentOffset {
let newContentOffset = CGPoint(x: 0, y: scrollView.contentOffset.y + retryViewHeight)
scrollView.setContentOffset(newContentOffset, animated: true)
if #available(iOS 13, *) {
scrollView.setContentOffset(newContentOffset, animated: true)
}
}
}
}
}
@objc private func retryEvent() {
paginationViewModel.loadMore()
}
private func onEmptyState() {
defer {
wrappedView.scrollView.support.refreshControl?.endRefreshing()
@ -231,7 +236,7 @@ final public class PaginationWrapper<Cursor: ResettableRxDataSourceCursor, Deleg
private func replacePlaceholderViewIfNeeded(with placeholderView: UIView) {
wrappedView.scrollView.isUserInteractionEnabled = true
removeCurrentPlaceholderView()
removeAllPlaceholderView()
placeholderView.translatesAutoresizingMaskIntoConstraints = false
placeholderView.isHidden = false
@ -263,9 +268,10 @@ final public class PaginationWrapper<Cursor: ResettableRxDataSourceCursor, Deleg
private func onExhaustedState() {
removeInfiniteScroll()
removeAllPlaceholderView()
}
private func readdInfiniteScrollWithHandler() {
private func addInfiniteScrollWithHandler() {
removeInfiniteScroll()
addInfiniteScroll(withHandler: true)
}
@ -289,55 +295,32 @@ final public class PaginationWrapper<Cursor: ResettableRxDataSourceCursor, Deleg
private func createRefreshControl() {
let refreshControl = UIRefreshControl()
refreshControl.rx
.controlEvent(.valueChanged)
.asDriver()
.drive(reloadEvent)
.disposed(by: disposeBag)
refreshControl.addTarget(self, action: #selector(refreshAction), for: .valueChanged)
wrappedView.scrollView.support.setRefreshControl(refreshControl)
}
@objc private func refreshAction() {
// it is implemented the combined behavior of `touchUpInside` and `touchUpOutside` using `CFRunLoopPerformBlock`,
// which `UIRefreshControl` does not support
CFRunLoopPerformBlock(CFRunLoopGetMain(), CFRunLoopMode.defaultMode.rawValue) { [weak self] in
self?.reload()
}
}
private func removeRefreshControl() {
wrappedView.scrollView.support.setRefreshControl(nil)
}
private func bindViewModelStates() {
paginationViewModel.stateDriver
.flatMap { [applicationCurrentyActive] state -> Driver<LoadingState> in
if applicationCurrentyActive.value {
return .just(state)
} else {
return applicationCurrentyActive
.asObservable()
.filter { $0 }
.delay(0.5, scheduler: MainScheduler.instance)
.asDriver(onErrorJustReturn: true)
.replace(with: state)
}
}
.drive(stateChanged)
.disposed(by: disposeBag)
}
private func removeCurrentPlaceholderView() {
private func removeAllPlaceholderView() {
wrappedView.backgroundView = nil
}
private func bindAppStateNotifications() {
let notificationCenter = NotificationCenter.default.rx
notificationCenter.notification(UIApplication.willResignActiveNotification)
.replace(with: false)
.asDriver(onErrorJustReturn: false)
.drive(applicationCurrentyActive)
.disposed(by: disposeBag)
notificationCenter.notification(UIApplication.didBecomeActiveNotification)
.replace(with: true)
.asDriver(onErrorJustReturn: true)
.drive(applicationCurrentyActive)
.disposed(by: disposeBag)
wrappedView.footerView = nil
}
}
@ -349,10 +332,10 @@ private extension PaginationWrapper {
case .initial:
base.onInitialState()
case .initialLoading(let after):
case let .initialLoading(after):
base.onLoadingState(afterState: after)
case .loadingMore(let after):
case let .loadingMore(after):
base.onLoadingMoreState(afterState: after)
case let .results(newItems, from, after):
@ -370,18 +353,6 @@ private extension PaginationWrapper {
}
}
var retryEvent: Binder<Void> {
return Binder(self) { base, _ in
base.paginationViewModel.loadMore()
}
}
var reloadEvent: Binder<Void> {
return Binder(self) { base, _ in
base.reload()
}
}
var scrollOffsetChanged: Binder<CGPoint> {
return Binder(self) { base, value in
base.currentPlaceholderViewTopConstraint?.constant = -value.y

View File

@ -81,6 +81,7 @@ open class RxNetworkOperationModel<LoadingStateType: NetworkOperationState>: Net
func requestResult(from dataSource: DataSourceType) {
currentRequestDisposable = dataSource
.resultSingle()
.observeOn(MainScheduler.instance)
.subscribe(onSuccess: { [weak self] result in
self?.onGot(result: result, from: dataSource)
}, onError: { [weak self] error in

View File

@ -0,0 +1,44 @@
//
// 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 RxCocoa
import RxSwift
open class BaseTappableViewModel<PayloadType> {
private let tapRelay = PublishRelay<PayloadType>()
public var tapDriver: Driver<PayloadType> {
tapRelay.asDriver(onErrorDriveWith: .empty())
}
public var tapObservable: Observable<PayloadType> {
tapRelay.asObservable()
}
public func bind(tapObservable: Observable<PayloadType>) -> Disposable {
tapObservable.bind(to: tapRelay)
}
public func tap(payload: PayloadType) {
tapRelay.accept(payload)
}
}

View File

@ -0,0 +1,27 @@
//
// 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.
//
open class VoidTappableViewModel: BaseTappableViewModel<Void> {
public func tap() {
tap(payload: ())
}
}

View File

@ -24,7 +24,7 @@ import UIKit
/// Layoutless placeholder view. This class is used as views holder & configurator.
/// You should inherit it and implement layout.
open class BasePlaceholderView: UIView, InitializableView {
open class BasePlaceholderView: ButtonHolderView, InitializableView {
/// Title label of placeholder view.
public let titleLabel = UILabel()

View File

@ -0,0 +1,73 @@
//
// 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 RxSwift
open class BaseRxTableViewCell: UITableViewCell, InitializableView, DisposeBagHolder {
// MARK: - Properties
public var disposeBag = DisposeBag()
// MARK: - Initialization
override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: .default, reuseIdentifier: reuseIdentifier)
initializeView()
}
@available(*, unavailable)
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
// MARK: - Override
override open func prepareForReuse() {
super.prepareForReuse()
disposeBag = DisposeBag()
}
// MARK: - InitializableView
open func addViews() {
// overriding
}
open func bindViews() {
// overriding
}
open func configureLayout() {
// overriding
}
open func configureAppearance() {
selectionStyle = .none
}
open func localize() {
// overriding
}
}

View File

@ -0,0 +1,79 @@
//
// 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 RxSwift
import TableKit
open class ContainerTableCell<TView: UIView>: BaseRxTableViewCell, ConfigurableCell where TView: ConfigurableView {
// MARK: - Properties
private let wrappedView = TView()
open var shouldConfigureDefaultConstraints: Bool {
true
}
open var contentInsets: UIEdgeInsets {
.zero
}
open var contentViewBackgroundColor: UIColor {
.clear
}
// MARK: - ConfigurableCell
open func configure(with viewModel: TView.ViewModelType) {
disposeBag = DisposeBag()
wrappedView.configure(with: viewModel)
}
// MARK: - InitializableView
override open func addViews() {
super.addViews()
contentView.addSubview(wrappedView)
}
override open func configureLayout() {
super.configureLayout()
if shouldConfigureDefaultConstraints {
wrappedView.snp.makeConstraints {
$0.edges.equalToSuperview().inset(contentInsets)
}
} else {
configureCustomConstraints(forWrappedView: wrappedView)
}
}
override open func configureAppearance() {
super.configureAppearance()
contentView.backgroundColor = contentViewBackgroundColor
backgroundColor = contentViewBackgroundColor
}
open func configureCustomConstraints(forWrappedView view: TView) { }
}

View File

@ -51,13 +51,15 @@ public struct CustomizableButtonState: OptionSet {
}
/// container class that acts like a button and provides great customization
open class CustomizableButtonView: UIView, InitializableView {
open class CustomizableButtonView: UIView, InitializableView, ConfigurableView {
// MARK: - Stored Properties
private let disposeBag = DisposeBag()
public private(set) var disposeBag = DisposeBag()
private let button = CustomizableButton()
public var tapOnDisabledButton: VoidBlock?
open var tapOnDisabledButton: VoidBlock?
public var shadowView = UIView() {
willSet {
@ -71,9 +73,7 @@ open class CustomizableButtonView: UIView, InitializableView {
public var spinnerView: Spinner? {
willSet {
if newValue == nil {
removeSpinner()
}
removeSpinner()
}
didSet {
if spinnerView != nil {
@ -89,7 +89,12 @@ open class CustomizableButtonView: UIView, InitializableView {
}
}
public var buttonIsDisabledWhileLoading = false
public var buttonTitle: String = "" {
willSet {
button.text = newValue
}
}
public var hidesLabelWhenLoading = false
// MARK: - Computed Properties
@ -138,8 +143,6 @@ open class CustomizableButtonView: UIView, InitializableView {
}
private func set(active: Bool) {
button.isEnabled = buttonIsDisabledWhileLoading || !active
if hidesLabelWhenLoading {
button.titleLabel?.layer.opacity = active ? 0 : 1
}
@ -172,6 +175,7 @@ open class CustomizableButtonView: UIView, InitializableView {
private func configureConstraints() {
button.pinToSuperview(with: appearance.buttonInsets)
configureShadowViewConstraints()
layoutIfNeeded()
}
private func configureSpinnerConstraints() {
@ -208,20 +212,20 @@ open class CustomizableButtonView: UIView, InitializableView {
}
private func configureShadowViewConstraints() {
shadowView.constaintToEdges(of: button, with: .zero)
shadowView.constraintToEdges(of: button, with: .zero)
}
// MARK: - Initializable View
public func addViews() {
open func addViews() {
addSubviews(shadowView, button)
}
public func configureAppearance() {
open func configureAppearance() {
button.titleLabel?.numberOfLines = appearance.numberOfLines
button.titleLabel?.font = appearance.buttonFont
button.alpha = appearance.alpha
button.set(titles: appearance.buttonStateTitles)
button.set(attributtedTitles: appearance.buttonStateAttributtedTitles)
button.set(titleColors: appearance.buttonTitleStateColors)
button.set(images: appearance.buttonStateIcons)
@ -239,28 +243,15 @@ open class CustomizableButtonView: UIView, InitializableView {
button.layer.cornerRadius = 0
}
button.titleLabel?.isHidden = true
setNeedsDisplay()
}
}
private extension UIView {
func constaintToEdges(of view: UIView, with offset: UIEdgeInsets) {
translatesAutoresizingMaskIntoConstraints = false
let constraints = [
leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: offset.left),
trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: offset.right),
topAnchor.constraint(equalTo: view.topAnchor, constant: offset.top),
bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: offset.bottom)
]
NSLayoutConstraint.activate(constraints)
}
}
extension CustomizableButtonView: ConfigurableView {
public func configure(with viewModel: CustomizableButtonViewModel) {
open func configure(with viewModel: CustomizableButtonViewModel) {
disposeBag = DisposeBag()
viewModel.stateDriver.drive(stateBinder).disposed(by: disposeBag)
viewModel.bind(tapObservable: tapObservable).disposed(by: disposeBag)
button.text = viewModel.buttonTitle
appearance = viewModel.appearance
}
@ -276,34 +267,43 @@ extension CustomizableButtonView: ConfigurableView {
}
open func configureButton(withState state: CustomizableButtonState) {
button.isEnabled = state.contains(.enabled) && !state.contains(.disabled)
button.isEnabled = ![.disabled, .loading].contains(state)
isUserInteractionEnabled = button.isEnabled
button.isHighlighted = state.contains(.highlighted) && !state.contains(.normal)
set(active: state.contains(.loading))
setNeedsDisplay()
}
}
private extension UIView {
func constraintToEdges(of view: UIView, with offset: UIEdgeInsets) {
translatesAutoresizingMaskIntoConstraints = false
let constraints = [
leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: offset.left),
trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: offset.right),
topAnchor.constraint(equalTo: view.topAnchor, constant: offset.top),
bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: offset.bottom)
]
NSLayoutConstraint.activate(constraints)
}
}
public extension CustomizableButtonView {
struct Appearance {
var buttonFont: UIFont
var buttonStateTitles: [UIControl.State: String]
var buttonStateAttributtedTitles: [UIControl.State: NSAttributedString]
var buttonTitleStateColors: [UIControl.State: UIColor]
var buttonBackgroundStateColors: [UIControl.State: UIColor]
var buttonStateIcons: [UIControl.State: UIImage]
var buttonIconOffset: UIOffset
var buttonInsets: UIEdgeInsets
var buttonCornerRadius: CGFloat?
var spinnerPosition: SpinnerPosition
var numberOfLines: Int
public var buttonFont: UIFont
public var buttonStateAttributtedTitles: [UIControl.State: NSAttributedString]
public var buttonTitleStateColors: [UIControl.State: UIColor]
public var buttonBackgroundStateColors: [UIControl.State: UIColor]
public var buttonStateIcons: [UIControl.State: UIImage]
public var buttonIconOffset: UIOffset
public var buttonInsets: UIEdgeInsets
public var buttonCornerRadius: CGFloat?
public var spinnerPosition: SpinnerPosition
public var numberOfLines: Int
public var alpha: CGFloat
public init(buttonFont: UIFont = .systemFont(ofSize: 15),
buttonStateTitles: [UIControl.State: String] = [:],
buttonStateAttributtedTitles: [UIControl.State: NSAttributedString] = [:],
buttonTitleStateColors: [UIControl.State: UIColor] = [:],
buttonBackgroundStateColors: [UIControl.State: UIColor] = [:],
@ -312,24 +312,20 @@ public extension CustomizableButtonView {
buttonInsets: UIEdgeInsets = .zero,
buttonCornerRadius: CGFloat? = nil,
spinnerPosition: SpinnerPosition = .center,
numberOfLines: Int = 0) {
numberOfLines: Int = 0,
alpha: CGFloat = 1) {
self.buttonFont = buttonFont
self.buttonStateTitles = buttonStateTitles
self.buttonStateAttributtedTitles = buttonStateAttributtedTitles
self.buttonTitleStateColors = buttonTitleStateColors
self.buttonBackgroundStateColors = buttonBackgroundStateColors
self.buttonStateIcons = buttonStateIcons
self.buttonIconOffset = buttonIconOffset
self.buttonInsets = buttonInsets
self.buttonCornerRadius = buttonCornerRadius
self.spinnerPosition = spinnerPosition
self.numberOfLines = numberOfLines
self.alpha = alpha
}
}

View File

@ -31,8 +31,10 @@ open class CustomizableButtonViewModel {
private let stateRelay = BehaviorRelay(value: CustomizableButtonState.enabled)
private let tapRelay = BehaviorRelay(value: ())
public let appearance: Appearance
public let buttonTitle: String
public init(appearance: Appearance) {
public init(buttonTitle: String, appearance: Appearance) {
self.buttonTitle = buttonTitle
self.appearance = appearance
}

View File

@ -49,7 +49,7 @@ public extension PaginationWrapperUIDelegate {
return AnyLoadingIndicator(indicator)
}
func footerRetryButton() -> UIButton? {
func footerRetryView() -> ButtonHolderView? {
let retryButton = UIButton(type: .custom)
retryButton.backgroundColor = .lightGray
retryButton.setTitle("Retry load more", for: .normal)
@ -57,15 +57,15 @@ public extension PaginationWrapperUIDelegate {
return retryButton
}
func footerRetryButtonHeight() -> CGFloat {
func footerRetryViewHeight() -> CGFloat {
return 44
}
func footerRetryButtonWillAppear() {
func footerRetryViewWillAppear() {
// by default - nothing will happen
}
func footerRetryButtonWillDisappear() {
func footerRetryViewWillDisappear() {
// by default - nothing will happen
}
}

View File

@ -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.UIButton
extension UIButton: ButtonHolder {
public var button: UIButton {
self
}
}

View File

@ -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 UIKit.UIButton
/// Protocol that contains button property.
public protocol ButtonHolder {
/// Contained UIButton instance.
var button: UIButton { get }
}

View File

@ -0,0 +1,26 @@
//
// 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.UIView
/// View which contains button
public typealias ButtonHolderView = UIView & ButtonHolder

View File

@ -55,21 +55,21 @@ public protocol PaginationWrapperUIDelegate: class {
/// - Returns: Configured instace of AnyLoadingIndicator.
func loadingMoreIndicator() -> AnyLoadingIndicator?
/// Returns instance of UIButton for "retry load more" action.
/// Returns instance of ButtonHolderView with retry button for "retry load more" action.
///
/// - Returns: Configured instace of AnyLoadingIndicator.
func footerRetryButton() -> UIButton?
func footerRetryView() -> ButtonHolderView?
/// Returns height for "retry load more" button.
///
/// - Returns: Height of "retry load more" button.
func footerRetryButtonHeight() -> CGFloat
func footerRetryViewHeight() -> CGFloat
/// Method is called before "retry load more" will be shown.
/// Typically, it's used when you need to show custom footer view.
func footerRetryButtonWillAppear()
func footerRetryViewWillAppear()
/// Method is called before "retry load more" will be hidden.
/// Typically, it's used when you need to hide custom footer view.
func footerRetryButtonWillDisappear()
func footerRetryViewWillDisappear()
}

7
TISwiftUtils/README.md Normal file
View File

@ -0,0 +1,7 @@
# TISwiftUtils
Bunch of useful helpers for development.
# Installation via SPM
You can install this framework as a target of LeadKit.

View File

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

View File

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

View File

@ -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> = (Input) -> Output
/// Closure with no arguments and custom return value.
public typealias ResultClosure<Output> = () -> Output
/// Closure that takes custom arguments and returns Void.
public typealias ParameterClosure<Input> = Closure<Input, Void>
// MARK: Throwable versions
/// Closure with custom arguments and return value, may throw an error.
public typealias ThrowableClosure<Input, Output> = (Input) throws -> Output
/// Closure with no arguments and custom return value, may throw an error.
public typealias ThrowableResultClosure<Output> = () throws -> Output
/// Closure that takes custom arguments and returns Void, may throw an error.
public typealias ThrowableParameterClosure<Input> = ThrowableClosure<Input, Void>
// MARK: Concrete closures
/// Closure that takes no arguments and returns Void.
public typealias VoidClosure = ResultClosure<Void>
/// Closure that takes no arguments, may throw an error and returns Void.
public typealias ThrowableVoidClosure = () throws -> Void

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

60
TITransitions/README.md Normal file
View File

@ -0,0 +1,60 @@
# TITransitions
![Version](https://img.shields.io/github/v/release/Loupehope/TITransitions)
![Platform](https://img.shields.io/badge/platform-iOS-green)
![License](https://img.shields.io/hexpm/l/plug?color=darkBlue)
Set of custom transitions to present controller.
# PanelTransition
Use to present ViewController from the bottom.
## Usage
```swift
let panelTransition = PanelTransition(presentStyle: .halfScreen)
let childController = UIViewController()
childController.view.backgroundColor = .white
childController.transitioningDelegate = panelTransition
childController.modalPresentationStyle = .custom
rootController.present(childController)
```
<p align="left">
<img src="Assets/panel_transition.gif" width=300 height=600>
</p>
## Customization
To customize panel transition behaviour use the PanelTransition's constructor.
```swift
PanelTransition(presentStyle: PresentStyle, //required
panelConfig: PanelPresentationController.Configuration = .default,
driver: TransitionDriver? = .init(),
presentAnimation: PresentAnimation = .init(),
dismissAnimation: DismissAnimation = .init())
```
1. *PresentStyle* - defines a position of ViewController:
- fullScreen
- halfScreen
- customInsets(UIEdgeInsets)
- customHeight(CGFloat)
2. *PanelPresentationController.Configuration* - defines a background color of back view and a tap gesture:
```swift
struct Configuration {
let backgroundColor: UIColor
let onTapDismissEnabled: Bool
let onTapDismissCompletion: VoidClosure?
```
3. *TransitionDriver* is responsible for swipe gesture.
4. *PresentAnimation and DismissAnimation* defines present and dismiss animations.
# Installation via SPM
You can install this framework as a target of LeadKit.
# License
TITransitions is available under the Apache License 2.0. See the LICENSE file for more info.

View File

@ -0,0 +1,49 @@
//
// 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
open class BaseAnimation: NSObject, Animation {
public let duration: TimeInterval
public init(duration: TimeInterval = 0.3) {
self.duration = duration
}
public func animator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
UIViewPropertyAnimator()
}
}
extension BaseAnimation: UIViewControllerAnimatedTransitioning {
public func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
duration
}
public func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
animator(using: transitionContext).startAnimation()
}
public func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
animator(using: transitionContext)
}
}

View File

@ -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 protocol Animation {
var duration: TimeInterval { get }
func animator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating
}

View File

@ -0,0 +1,25 @@
//
// 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 typealias VoidClosure = (() -> Void)

View File

@ -0,0 +1,44 @@
//
// 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
open class DismissAnimation: BaseAnimation {
override open func animator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
guard let fromView = transitionContext.view(forKey: .from),
let fromController = transitionContext.viewController(forKey: .from) else {
return UIViewPropertyAnimator()
}
let initialFrame = transitionContext.initialFrame(for: fromController)
let animator = UIViewPropertyAnimator(duration: duration, curve: .easeOut) {
fromView.frame = initialFrame.offsetBy(dx: .zero, dy: initialFrame.height)
}
animator.addCompletion { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
return animator
}
}

View File

@ -0,0 +1,47 @@
//
// 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
open class PresentAnimation: BaseAnimation {
override open func animator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
guard let toView = transitionContext.view(forKey: .to),
let toController = transitionContext.viewController(forKey: .to) else {
return UIViewPropertyAnimator()
}
let finalFrame = transitionContext.finalFrame(for: toController)
toView.frame = finalFrame.offsetBy(dx: .zero, dy: finalFrame.height)
let animator = UIViewPropertyAnimator(duration: duration, curve: .easeOut) {
toView.frame = finalFrame
}
animator.addCompletion { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
return animator
}
}

View File

@ -0,0 +1,128 @@
//
// 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
open class PanelPresentationController: PresentationController {
private let config: Configuration
private lazy var backView: UIView = {
let view = UIView()
view.backgroundColor = config.backgroundColor
view.alpha = 0
return view
}()
public init(config: Configuration,
driver: TransitionDriver?,
presentStyle: PresentStyle,
presentedViewController: UIViewController,
presenting: UIViewController?) {
self.config = config
super.init(driver: driver,
presentStyle: presentStyle,
presentedViewController: presentedViewController,
presenting: presenting)
if config.onTapDismissEnabled {
configureOnTapClose()
}
}
override open func presentationTransitionWillBegin() {
super.presentationTransitionWillBegin()
containerView?.insertSubview(backView, at: 0)
performAlongsideTransitionIfPossible { [weak self] in
self?.backView.alpha = 1
}
}
override open func containerViewDidLayoutSubviews() {
super.containerViewDidLayoutSubviews()
backView.frame = containerView?.frame ?? .zero
}
override open func presentationTransitionDidEnd(_ completed: Bool) {
super.presentationTransitionDidEnd(completed)
if !completed {
backView.removeFromSuperview()
}
}
override open func dismissalTransitionWillBegin() {
super.dismissalTransitionWillBegin()
performAlongsideTransitionIfPossible { [weak self] in
self?.backView.alpha = .zero
}
}
override open func dismissalTransitionDidEnd(_ completed: Bool) {
super.dismissalTransitionDidEnd(completed)
if completed {
backView.removeFromSuperview()
}
}
private func configureOnTapClose() {
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(onTapClose))
backView.addGestureRecognizer(tapGesture)
}
@objc private func onTapClose() {
presentedViewController.dismiss(animated: true, completion: config.onTapDismissCompletion)
}
private func performAlongsideTransitionIfPossible(_ block: @escaping () -> Void) {
guard let coordinator = presentedViewController.transitionCoordinator else {
block()
return
}
coordinator.animate(alongsideTransition: { _ in
block()
})
}
}
extension PanelPresentationController {
public struct Configuration {
public let backgroundColor: UIColor
public let onTapDismissEnabled: Bool
public let onTapDismissCompletion: VoidClosure?
public init(backgroundColor: UIColor = UIColor.black.withAlphaComponent(0.4),
onTapDismissEnabled: Bool = true,
onTapDismissCompletion: VoidClosure? = nil) {
self.backgroundColor = backgroundColor
self.onTapDismissEnabled = onTapDismissEnabled
self.onTapDismissCompletion = onTapDismissCompletion
}
public static let `default`: Configuration = .init()
}
}

View File

@ -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 UIKit
public enum PresentStyle {
case fullScreen
case halfScreen
case customInsets(UIEdgeInsets)
case customHeight(CGFloat)
}

View File

@ -0,0 +1,100 @@
//
// 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
open class PresentationController: UIPresentationController {
private let presentStyle: PresentStyle
private let driver: TransitionDriver?
override open var shouldPresentInFullscreen: Bool {
false
}
override open var frameOfPresentedViewInContainerView: CGRect {
calculatePresentedFrame(for: presentStyle)
}
public init(driver: TransitionDriver?,
presentStyle: PresentStyle,
presentedViewController: UIViewController,
presenting: UIViewController?) {
self.driver = driver
self.presentStyle = presentStyle
super.init(presentedViewController: presentedViewController, presenting: presenting)
}
override open func presentationTransitionWillBegin() {
super.presentationTransitionWillBegin()
if let presentedView = presentedView {
containerView?.addSubview(presentedView)
}
}
override open func containerViewDidLayoutSubviews() {
super.containerViewDidLayoutSubviews()
presentedView?.frame = frameOfPresentedViewInContainerView
}
override open func presentationTransitionDidEnd(_ completed: Bool) {
super.presentationTransitionDidEnd(completed)
if completed {
driver?.direction = .dismiss
}
}
}
private extension PresentationController {
func calculatePresentedFrame(for style: PresentStyle) -> CGRect {
guard let bounds = containerView?.bounds else {
return .zero
}
switch style {
case .fullScreen:
return CGRect(x: .zero, y: .zero, width: bounds.width, height: bounds.height)
case .halfScreen:
let halfHeight = bounds.height / 2
return CGRect(x: .zero, y: halfHeight, width: bounds.width, height: halfHeight)
case let .customInsets(insets):
return calculateCustomFrame(insets: insets)
case let .customHeight(height):
return CGRect(x: .zero, y: bounds.height - height, width: bounds.width, height: height)
}
}
func calculateCustomFrame(insets: UIEdgeInsets) -> CGRect {
guard let bounds = containerView?.bounds else {
return .zero
}
let origin = CGPoint(x: insets.left, y: insets.top)
let size = CGSize(width: bounds.width - insets.right - insets.left,
height: bounds.height - insets.top - insets.bottom)
return CGRect(origin: origin, size: size)
}
}

View File

@ -0,0 +1,52 @@
//
// 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
extension UIPanGestureRecognizer {
func projectedLocation(decelerationRate: UIScrollView.DecelerationRate) -> CGPoint {
let velocityOffset = velocity(in: view).projectedOffset(decelerationRate: .normal)
let projectedLocation = location(in: view) + velocityOffset
return projectedLocation
}
}
extension CGPoint {
func projectedOffset(decelerationRate: UIScrollView.DecelerationRate) -> CGPoint {
return CGPoint(x: x.projectedOffset(decelerationRate: decelerationRate),
y: y.projectedOffset(decelerationRate: decelerationRate))
}
}
extension CGFloat {
func projectedOffset(decelerationRate: UIScrollView.DecelerationRate) -> CGFloat {
let multiplier = 1 / (1 - decelerationRate.rawValue) / 1000
return self * multiplier
}
}
extension CGPoint {
static func +(left: CGPoint, right: CGPoint) -> CGPoint {
return CGPoint(x: left.x + right.x,
y: left.y + right.y)
}
}

View File

@ -0,0 +1,26 @@
//
// 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.
//
public enum TransitionDirection {
case present
case dismiss
}

View File

@ -0,0 +1,142 @@
//
// 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
open class TransitionDriver: UIPercentDrivenInteractiveTransition {
private weak var presentedController: UIViewController?
private var panRecognizer: UIPanGestureRecognizer?
public var direction: TransitionDirection = .present
// MARK: - Linking
public func link(to controller: UIViewController) {
presentedController = controller
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handleGesture))
panRecognizer = panGesture
presentedController?.view.addGestureRecognizer(panGesture)
}
// MARK: - Override
override open var wantsInteractiveStart: Bool {
get {
switch direction {
case .present:
return false
case .dismiss:
return panRecognizer?.state == .began
}
}
set {}
}
@objc private func handleGesture(recognizer: UIPanGestureRecognizer) {
switch direction {
case .present:
handlePresentation(recognizer: recognizer)
case .dismiss:
handleDismiss(recognizer: recognizer)
}
}
}
// MARK: - Gesture Handling
private extension TransitionDriver {
var maxTranslation: CGFloat {
presentedController?.view.frame.height ?? 0
}
/// `pause()` before call `isRunning`
var isRunning: Bool {
percentComplete != 0
}
func handlePresentation(recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .began:
pause()
case .changed:
update(percentComplete - recognizer.incrementToBottom(maxTranslation: maxTranslation))
case .ended, .cancelled:
if recognizer.isProjectedToDownHalf(maxTranslation: maxTranslation) {
cancel()
} else {
finish()
}
case .failed:
cancel()
default:
break
}
}
func handleDismiss(recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .began:
pause() // Pause allows to detect isRunning
if !isRunning {
presentedController?.dismiss(animated: true) // Start the new one
}
case .changed:
update(percentComplete + recognizer.incrementToBottom(maxTranslation: maxTranslation))
case .ended, .cancelled:
if recognizer.isProjectedToDownHalf(maxTranslation: maxTranslation) {
finish()
} else {
cancel()
}
case .failed:
cancel()
default:
break
}
}
}
private extension UIPanGestureRecognizer {
func isProjectedToDownHalf(maxTranslation: CGFloat) -> Bool {
let endLocation = projectedLocation(decelerationRate: .fast)
let isPresentationCompleted = endLocation.y > maxTranslation / 2
return isPresentationCompleted
}
func incrementToBottom(maxTranslation: CGFloat) -> CGFloat {
let translation = self.translation(in: view).y
setTranslation(.zero, in: nil)
let percentIncrement = translation / maxTranslation
return percentIncrement
}
}

View File

@ -0,0 +1,79 @@
//
// 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
open class PanelTransition: NSObject, UIViewControllerTransitioningDelegate {
// MARK: - Presentation controller
private let driver: TransitionDriver?
private let presentStyle: PresentStyle
private let presentAnimation: PresentAnimation
private let dismissAnimation: DismissAnimation
private let panelConfig: PanelPresentationController.Configuration
public init(presentStyle: PresentStyle,
panelConfig: PanelPresentationController.Configuration = .default,
driver: TransitionDriver? = .init(),
presentAnimation: PresentAnimation = .init(),
dismissAnimation: DismissAnimation = .init()) {
self.presentStyle = presentStyle
self.driver = driver
self.presentAnimation = presentAnimation
self.dismissAnimation = dismissAnimation
self.panelConfig = panelConfig
super.init()
}
public func presentationController(forPresented presented: UIViewController,
presenting: UIViewController?,
source: UIViewController) -> UIPresentationController? {
driver?.link(to: presented)
let presentationController = PanelPresentationController(config: panelConfig,
driver: driver,
presentStyle: presentStyle,
presentedViewController: presented,
presenting: presenting ?? source)
return presentationController
}
// MARK: - Animation
public func animationController(forPresented presented: UIViewController,
presenting: UIViewController,
source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
presentAnimation
}
public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
dismissAnimation
}
// MARK: - Interaction
public func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
driver
}
public func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
driver
}
}

7
TIUIElements/README.md Normal file
View File

@ -0,0 +1,7 @@
# TIUIElements
Bunch of useful protocols and views.
# Installation via SPM
You can install this framework as a target of LeadKit.

View File

@ -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 UIKit
import TIUIKitCore
extension UIActivityIndicatorView: ActivityIndicatorHolder {
public var activityIndicator: Animatable {
self
}
}

View File

@ -0,0 +1,26 @@
//
// 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
extension UIActivityIndicatorView: Animatable {}

18
TIUIKitCore/README.md Normal file
View File

@ -0,0 +1,18 @@
# TIUIKitCore
Core UI elements: protocols, views and helpers.
# Protocols
- [InitializableView](InitializableView/InitializableView.swift) - protocol with methods that should be called in constructor methods of view.
- [Animatable](Animatable/Animatable.swift) - protocol that ensures that specific type support basic animation actions.
- [ActivityIndicator](ActivityIndicator/ActivityIndicator.swift) - basic activity indicator.
- [ActivityIndicatorHolder](ActivityIndicator/ActivityIndicatorHolder.swift) - placeholder view, containing activity indicator.
# Views
- [BaseInitializableView](BaseInitializableView/BaseInitializableView.swift) - UIView conformance to InitializableView.
# Installation via SPM
You can install this framework as a target of LeadKit.

View File

@ -0,0 +1,32 @@
//
// 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.
//
public extension InitializableView {
func initializeView() {
addViews()
configureLayout()
bindViews()
configureAppearance()
localize()
}
}

View File

@ -0,0 +1,33 @@
//
// 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 UIStackView {
func addArrangedSubviews(_ views: [UIView]) {
views.forEach { addArrangedSubview($0) }
}
func addArrangedSubviews(_ views: UIView...) {
views.forEach { addArrangedSubview($0) }
}
}

View File

@ -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 ActivityIndicatorHolder where Self: UIView {
var indicatorOwner: UIView {
self
}
}

View File

@ -0,0 +1,33 @@
//
// 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 UIView {
func addSubviews(_ views: [UIView]) {
views.forEach { addSubview($0) }
}
func addSubviews(_ views: UIView...) {
views.forEach { addSubview($0) }
}
}

View File

@ -0,0 +1,26 @@
//
// 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
/// Protocol that describes basic activity indicator.
public typealias ActivityIndicator = UIView & Animatable

View File

@ -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
/// Protocol that describes placeholder view, containing activity indicator.
public protocol ActivityIndicatorHolder: class {
var activityIndicator: Animatable { get }
var indicatorOwner: UIView { get }
}

View File

@ -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.
//
/// Protocol that ensures that specific type support basic animation actions.
public protocol Animatable {
/// Method that starts animation.
func startAnimating()
/// Method that stops animation.
func stopAnimating()
}

View File

@ -0,0 +1,43 @@
//
// 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.
//
/// Protocol with methods that should be called in constructor methods of view.
public protocol InitializableView {
/// Main method that should call other methods in particular order.
func initializeView()
/// Method for adding views to current view.
func addViews()
/// Confgiure layout of subviews.
func configureLayout()
/// Method for binding to data or user actions.
func bindViews()
/// Appearance configuration method.
func configureAppearance()
/// Localization method.
func localize()
}

View File

@ -0,0 +1,37 @@
import UIKit
open class BaseInitializableControl: UIControl, InitializableView {
override public init(frame: CGRect) {
super.init(frame: frame)
initializeView()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initializeView()
}
// MARK: - InitializableView
open func addViews() {
// override in subclass
}
open func configureLayout() {
// override in subclass
}
open func bindViews() {
// override in subclass
}
open func configureAppearance() {
// override in subclass
}
open func localize() {
// override in subclass
}
}

View File

@ -0,0 +1,59 @@
//
// 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
open class BaseInitializableView: UIView, InitializableView {
override public init(frame: CGRect) {
super.init(frame: frame)
initializeView()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initializeView()
}
// MARK: - InitializableView
open func addViews() {
// override in subclass
}
open func configureLayout() {
// override in subclass
}
open func bindViews() {
// override in subclass
}
open func configureAppearance() {
// override in subclass
}
open func localize() {
// override in subclass
}
}