Merge remote-tracking branch 'origin/master' into feature/podspecs_update
This commit is contained in:
commit
d595e0331f
54
CHANGELOG.md
54
CHANGELOG.md
|
|
@ -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`.
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -0,0 +1,151 @@
|
|||
# OTPSwiftView
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
]
|
||||
)
|
||||
11
README.md
11
README.md
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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: ())
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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) { }
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
# TISwiftUtils
|
||||
|
||||
Bunch of useful helpers for development.
|
||||
|
||||
# Installation via SPM
|
||||
|
||||
You can install this framework as a target of LeadKit.
|
||||
|
|
@ -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 ?? ""
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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 |
|
|
@ -0,0 +1,60 @@
|
|||
# TITransitions
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
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.
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# TIUIElements
|
||||
|
||||
Bunch of useful protocols and views.
|
||||
|
||||
# Installation via SPM
|
||||
|
||||
You can install this framework as a target of LeadKit.
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
|
|
@ -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.
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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 }
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue