diff --git a/CHANGELOG.md b/CHANGELOG.md index 5529e2a8..ffc9be9f 100644 --- a/CHANGELOG.md +++ b/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`. diff --git a/LeadKit.podspec b/LeadKit.podspec index 8fe76163..94ab383f 100644 --- a/LeadKit.podspec +++ b/LeadKit.podspec @@ -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" diff --git a/LeadKit.xcodeproj/project.pbxproj b/LeadKit.xcodeproj/project.pbxproj index 010fe015..66fb2a14 100644 --- a/LeadKit.xcodeproj/project.pbxproj +++ b/LeadKit.xcodeproj/project.pbxproj @@ -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 = ""; }; 40F118481F8FF223004AADAF /* TableRow+AppearanceExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TableRow+AppearanceExtension.swift"; sourceTree = ""; }; 411073AE23466B41002DD9B9 /* UIViewController+PresentFullScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+PresentFullScreen.swift"; sourceTree = ""; }; + 4CF65D1324DD684A0006B001 /* ButtonHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonHolder.swift; sourceTree = ""; }; + 4CF65D1524DD69250006B001 /* UIButton+ButtonHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIButton+ButtonHolder.swift"; sourceTree = ""; }; + 4CF65D1724DD6C080006B001 /* ButtonHolderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonHolderView.swift; sourceTree = ""; }; + 52421F8C24EAB52E00948DD1 /* ContainerTableCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContainerTableCell.swift; sourceTree = ""; }; + 52421F8E24EAB84900948DD1 /* BaseRxTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseRxTableViewCell.swift; sourceTree = ""; }; + 52421F9324EBCFAE00948DD1 /* VoidTappableViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoidTappableViewModel.swift; sourceTree = ""; }; + 52421F9524EBCFBB00948DD1 /* BaseTappableViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseTappableViewModel.swift; sourceTree = ""; }; 67051ADA1EBC7C36008EADC0 /* SpinnerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpinnerView.swift; sourceTree = ""; }; 6713C23620AF0C4D00875921 /* NetworkOperationState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkOperationState.swift; sourceTree = ""; }; 6713C23B20AF0D5900875921 /* NetworkOperationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkOperationModel.swift; sourceTree = ""; }; @@ -859,6 +873,48 @@ path = UITableView; sourceTree = ""; }; + 4CF65D1924DD6C3D0006B001 /* ButtonHolder */ = { + isa = PBXGroup; + children = ( + 4CF65D1324DD684A0006B001 /* ButtonHolder.swift */, + 4CF65D1724DD6C080006B001 /* ButtonHolderView.swift */, + ); + path = ButtonHolder; + sourceTree = ""; + }; + 52421F8B24EAB52E00948DD1 /* ContainerTableCell */ = { + isa = PBXGroup; + children = ( + 52421F8C24EAB52E00948DD1 /* ContainerTableCell.swift */, + ); + path = ContainerTableCell; + sourceTree = ""; + }; + 52421F9024EAB84E00948DD1 /* BaseRxTableViewCell */ = { + isa = PBXGroup; + children = ( + 52421F8E24EAB84900948DD1 /* BaseRxTableViewCell.swift */, + ); + path = BaseRxTableViewCell; + sourceTree = ""; + }; + 52421F9124EBCF6E00948DD1 /* ViewModels */ = { + isa = PBXGroup; + children = ( + 52421F9224EBCF8600948DD1 /* TappableViewModel */, + ); + path = ViewModels; + sourceTree = ""; + }; + 52421F9224EBCF8600948DD1 /* TappableViewModel */ = { + isa = PBXGroup; + children = ( + 52421F9524EBCFBB00948DD1 /* BaseTappableViewModel.swift */, + 52421F9324EBCFAE00948DD1 /* VoidTappableViewModel.swift */, + ); + path = TappableViewModel; + sourceTree = ""; + }; 671461C41EB3396E00EAB194 /* Classes */ = { isa = PBXGroup; children = ( @@ -867,6 +923,7 @@ 6774527E2062566D0024EEEF /* DataLoading */, 671461D21EB3396E00EAB194 /* Services */, 671461D41EB3396E00EAB194 /* Views */, + 52421F9124EBCF6E00948DD1 /* ViewModels */, ); path = Classes; sourceTree = ""; @@ -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 = ""; @@ -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 */, diff --git a/OTPSwiftView/Assets/preview.gif b/OTPSwiftView/Assets/preview.gif new file mode 100644 index 00000000..7738c77b Binary files /dev/null and b/OTPSwiftView/Assets/preview.gif differ diff --git a/OTPSwiftView/README.md b/OTPSwiftView/README.md new file mode 100644 index 00000000..61585f8b --- /dev/null +++ b/OTPSwiftView/README.md @@ -0,0 +1,151 @@ +# OTPSwiftView + +![Platform](https://img.shields.io/badge/platform-iOS-green) + +A fully customizable OTP view. + +

+ +

+ +# Usage +```swift +class ViewController: UIViewController { + let otpView = CustomOTPSwiftView() // Custom OTP view + + let config = OTPCodeConfig(codeSymbolsCount: 6, // Base configuration of OTP view + spacing: 6, + customSpacing: [2: 20]) + + override func viewDidLoad() { + super.viewDidLoad() + + /* + Add your codeView and set layout + */ + + /* Configure OTP view */ + + otpView.configure(with: config) + + /* Bind events */ + + otpView.onTextEnter = { code in + // Get code from codeView + } + + /* Update text */ + + otpView.code = "234435" + + /* Update focus */ + + otpView.beginFirstResponder() // show keyboard + otpView.resignFirstResponder() // hide keyboard + } +} +``` + +# Customization +## Single OTP View +*OTPView* is a base class that describes a single OTP textfield. +To customize the appearance and layout, you must inherit from the OTPView. +*Don't forget to add UIGestureRecognizer to call closure `onTap?()`. Use UITapGestureRecognizer to avoid bugs.* + +```swift +import OTPSwiftView + +class CustomOTPView: OTPView { + override func addViews() { + super.addViews() + + // Adding additional views to current view. The OTP textfield has already been added. + } + + override func configureLayout() { + super.configureLayout() + + // Confgiure layout of subviews + } + + override func bindViews() { + super.bindViews() + + // Binding to data or user actions + + let gesture = UITapGestureRecognizer(target: self, action: #selector(onTapAction)) + addGestureRecognizer(gesture) + } + + private func onTapAction() { + onTap?() + } + + override func configureAppearance() { + super.configureAppearance() + + // Appearance configuration method + } +} +``` + +*If needed to set validation for input use `validationClosure: ValidationClosure?`*. For example, only numbers validation: + +```swift +import OTPSwiftView + +class CustomOTPView: OTPView { + + override func bindViews() { + super.bindViews() + + codeTextField.validationClosure = { input in + input.allSatisfy { $0.isNumber } + } + } +} +``` + +## OTPSwiftView +*OTPSwiftView* is a base class that is responsible for the layout of single OTP views. +As with OTPView, you should create an heir class to configure your full OTP view. + +```swift +import OTPSwiftView + +final class CustomOTPSwiftView: OTPSwiftView { + override func addViews() { + super.addViews() + + // Adding additional views to current code view. The single OTP views has already been added. + } + + override func configureLayout() { + super.configureLayout() + + // Confgiure layout of subviews + } + + override func bindViews() { + super.bindViews() + + // Binding to data or user actions + } + + override func configureAppearance() { + super.configureAppearance() + + // Appearance configuration method + } + + override func configure(with config: OTPCodeConfig) { + super.configure(with: config) + + // Configure you code view with configuration + } +} +``` + +# Installation via SPM + +You can install this framework as a target of LeadKit. diff --git a/OTPSwiftView/Sources/Models/OTPCodeConfig.swift b/OTPSwiftView/Sources/Models/OTPCodeConfig.swift new file mode 100644 index 00000000..3fa0ae67 --- /dev/null +++ b/OTPSwiftView/Sources/Models/OTPCodeConfig.swift @@ -0,0 +1,38 @@ +// +// Copyright (c) 2020 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import UIKit + +/// Base configuration for OTPSwiftView +open class OTPCodeConfig { + public typealias Spacing = [Int: CGFloat] + + public let codeSymbolsCount: Int + public let spacing: CGFloat + public let customSpacing: Spacing? + + public init(codeSymbolsCount: Int, spacing: CGFloat, customSpacing: Spacing?) { + self.codeSymbolsCount = codeSymbolsCount + self.spacing = spacing + self.customSpacing = customSpacing + } +} diff --git a/OTPSwiftView/Sources/Views/OTPSwiftView/OTPSwiftView.swift b/OTPSwiftView/Sources/Views/OTPSwiftView/OTPSwiftView.swift new file mode 100644 index 00000000..1df6a0b1 --- /dev/null +++ b/OTPSwiftView/Sources/Views/OTPSwiftView/OTPSwiftView.swift @@ -0,0 +1,149 @@ +// +// Copyright (c) 2020 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import UIKit +import TIUIKitCore +import TISwiftUtils + +/// Base full OTP View for entering the verification code +open class OTPSwiftView: BaseInitializableControl { + private var emptyOTPView: View? { + textFieldsCollection.first { $0.codeTextField.text.orEmpty.isEmpty } ?? textFieldsCollection.last + } + + public private(set) var codeStackView = UIStackView() + public private(set) var textFieldsCollection: [View] = [] + + public var onTextEnter: ParameterClosure? + + public var code: String { + get { + textFieldsCollection.compactMap { $0.codeTextField.text }.joined() + } + set { + textFieldsCollection.first?.codeTextField.set(inputText: newValue) + } + } + + public override var isFirstResponder: Bool { + !textFieldsCollection.allSatisfy { !$0.codeTextField.isFirstResponder } + } + + open override func addViews() { + super.addViews() + + addSubview(codeStackView) + } + + open override func configureAppearance() { + super.configureAppearance() + + codeStackView.contentMode = .center + codeStackView.distribution = .fillEqually + } + + open func configure(with config: OTPCodeConfig) { + textFieldsCollection = createTextFields(numberOfFields: config.codeSymbolsCount) + + codeStackView.addArrangedSubviews(textFieldsCollection) + codeStackView.spacing = config.spacing + + configure(customSpacing: config.customSpacing, for: codeStackView) + + bindTextFields(with: config) + } + + @discardableResult + open override func becomeFirstResponder() -> Bool { + guard let emptyOTPView = emptyOTPView, !emptyOTPView.isFirstResponder else { + return false + } + + return emptyOTPView.codeTextField.becomeFirstResponder() + } + + @discardableResult + open override func resignFirstResponder() -> Bool { + guard let emptyOTPView = emptyOTPView, emptyOTPView.isFirstResponder else { + return false + } + + return emptyOTPView.codeTextField.resignFirstResponder() + } +} + +// MARK: - Configure textfields + +private extension OTPSwiftView { + func configure(customSpacing: OTPCodeConfig.Spacing?, for stackView: UIStackView) { + guard let customSpacing = customSpacing else { + return + } + + customSpacing.forEach { viewIndex, spacing in + guard viewIndex < stackView.arrangedSubviews.count, viewIndex >= .zero else { + return + } + + self.set(spacing: spacing, + after: stackView.arrangedSubviews[viewIndex], + for: stackView) + } + } + + func set(spacing: CGFloat, after view: UIView, for stackView: UIStackView) { + stackView.setCustomSpacing(spacing, after: view) + } + + func createTextFields(numberOfFields: Int) -> [View] { + var textFieldsCollection: [View] = [] + + (.zero..? + public var caretHeight: CGFloat? + + public var lastNotEmpty: OTPTextField { + let isLastNotEmpty = !text.orEmpty.isEmpty && nextTextField?.text.orEmpty.isEmpty ?? true + return isLastNotEmpty ? self : nextTextField?.lastNotEmpty ?? self + } + + open override var font: UIFont? { + didSet { + if caretHeight == nil, let font = font { + caretHeight = font.pointSize - font.descender + } + } + } + + public override init(frame: CGRect) { + super.init(frame: frame) + + delegate = self + } + + @available(*, unavailable) + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + open override func deleteBackward() { + guard text.orEmpty.isEmpty else { + return + } + + onTextChangedSignal?() + previousTextField?.text = "" + previousTextField?.becomeFirstResponder() + } + + public func set(inputText: String) { + text = inputText.prefix(maxSymbolsCount).string + + let nextInputText = inputText.count >= maxSymbolsCount + ? inputText.suffix(inputText.count - maxSymbolsCount).string + : "" + + nextTextField?.set(inputText: nextInputText) + } + + open override func caretRect(for position: UITextPosition) -> CGRect { + guard let caretHeight = caretHeight else { + return super.caretRect(for: position) + } + + var superRect = super.caretRect(for: position) + superRect.size.height = caretHeight + + return superRect + } + + open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let view = super.hitTest(point, with: event) + return view == self && isFirstResponder ? view : nil + } +} + +extension OTPTextField: UITextFieldDelegate { + public func textField(_ textField: UITextField, + shouldChangeCharactersIn range: NSRange, + replacementString string: String) -> Bool { + guard let textField = textField as? OTPTextField else { + return true + } + + let isInputEmpty = textField.text.orEmpty.isEmpty && string.isEmpty + + guard isInputEmpty || validationClosure?(string) ?? true else { + return false + } + + switch range.length { + case 0: // set text to textfield + textField.set(inputText: string) + + let currentTextField = textField.lastNotEmpty.nextTextField ?? textField.lastNotEmpty + currentTextField.becomeFirstResponder() + textField.onTextChangedSignal?() + + return false + + case 1: // remove character from textfield + textField.text = "" + textField.onTextChangedSignal?() + return false + + default: + return true + } + } +} diff --git a/OTPSwiftView/Sources/Views/OTPView/OTPView.swift b/OTPSwiftView/Sources/Views/OTPView/OTPView.swift new file mode 100644 index 00000000..c2a29468 --- /dev/null +++ b/OTPSwiftView/Sources/Views/OTPView/OTPView.swift @@ -0,0 +1,37 @@ +// +// Copyright (c) 2020 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import TIUIKitCore +import TISwiftUtils + +/// Base OTP view with textfield for entering a one symbol +open class OTPView: BaseInitializableView { + public let codeTextField = OTPTextField() + + public var onTap: VoidClosure? + + open override func addViews() { + super.addViews() + + addSubview(codeTextField) + } +} diff --git a/Package.swift b/Package.swift new file mode 100644 index 00000000..f15b6fb6 --- /dev/null +++ b/Package.swift @@ -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") + ] +) diff --git a/README.md b/README.md index bf114831..6cda623b 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,11 @@ # LeadKit -LeadKit it's a iOS framework with a bunch of tools for rapid app development \ No newline at end of file +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. + diff --git a/Sources/Classes/DataLoading/PaginationDataLoading/PaginationWrapper.swift b/Sources/Classes/DataLoading/PaginationDataLoading/PaginationWrapper.swift index a0dfb8ba..e1ecbed9 100644 --- a/Sources/Classes/DataLoading/PaginationDataLoading/PaginationWrapper.swift +++ b/Sources/Classes/DataLoading/PaginationDataLoading/PaginationWrapper.swift @@ -60,13 +60,15 @@ final public class PaginationWrapper= 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 Driver 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 { - return Binder(self) { base, _ in - base.paginationViewModel.loadMore() - } - } - - var reloadEvent: Binder { - return Binder(self) { base, _ in - base.reload() - } - } - var scrollOffsetChanged: Binder { return Binder(self) { base, value in base.currentPlaceholderViewTopConstraint?.constant = -value.y diff --git a/Sources/Classes/DataLoading/RxNetworkOperationModel.swift b/Sources/Classes/DataLoading/RxNetworkOperationModel.swift index a1d31adf..f0acc160 100644 --- a/Sources/Classes/DataLoading/RxNetworkOperationModel.swift +++ b/Sources/Classes/DataLoading/RxNetworkOperationModel.swift @@ -81,6 +81,7 @@ open class RxNetworkOperationModel: 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 diff --git a/Sources/Classes/ViewModels/TappableViewModel/BaseTappableViewModel.swift b/Sources/Classes/ViewModels/TappableViewModel/BaseTappableViewModel.swift new file mode 100644 index 00000000..c0d58183 --- /dev/null +++ b/Sources/Classes/ViewModels/TappableViewModel/BaseTappableViewModel.swift @@ -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 { + private let tapRelay = PublishRelay() + + public var tapDriver: Driver { + tapRelay.asDriver(onErrorDriveWith: .empty()) + } + + public var tapObservable: Observable { + tapRelay.asObservable() + } + + public func bind(tapObservable: Observable) -> Disposable { + tapObservable.bind(to: tapRelay) + } + + public func tap(payload: PayloadType) { + tapRelay.accept(payload) + } +} diff --git a/Sources/Classes/ViewModels/TappableViewModel/VoidTappableViewModel.swift b/Sources/Classes/ViewModels/TappableViewModel/VoidTappableViewModel.swift new file mode 100644 index 00000000..589ab45f --- /dev/null +++ b/Sources/Classes/ViewModels/TappableViewModel/VoidTappableViewModel.swift @@ -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 { + public func tap() { + tap(payload: ()) + } +} diff --git a/Sources/Classes/Views/BasePlaceholderView/BasePlaceholerView.swift b/Sources/Classes/Views/BasePlaceholderView/BasePlaceholerView.swift index 3e5e77d9..7687e1b3 100644 --- a/Sources/Classes/Views/BasePlaceholderView/BasePlaceholerView.swift +++ b/Sources/Classes/Views/BasePlaceholderView/BasePlaceholerView.swift @@ -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() diff --git a/Sources/Classes/Views/BaseRxTableViewCell/BaseRxTableViewCell.swift b/Sources/Classes/Views/BaseRxTableViewCell/BaseRxTableViewCell.swift new file mode 100644 index 00000000..4ec4ff74 --- /dev/null +++ b/Sources/Classes/Views/BaseRxTableViewCell/BaseRxTableViewCell.swift @@ -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 + } +} diff --git a/Sources/Classes/Views/ContainerTableCell/ContainerTableCell.swift b/Sources/Classes/Views/ContainerTableCell/ContainerTableCell.swift new file mode 100644 index 00000000..c30752d0 --- /dev/null +++ b/Sources/Classes/Views/ContainerTableCell/ContainerTableCell.swift @@ -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: 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) { } +} diff --git a/Sources/Classes/Views/CustomizableButton/CustomizableButtonView.swift b/Sources/Classes/Views/CustomizableButton/CustomizableButtonView.swift index 7eb614ea..80488726 100644 --- a/Sources/Classes/Views/CustomizableButton/CustomizableButtonView.swift +++ b/Sources/Classes/Views/CustomizableButton/CustomizableButtonView.swift @@ -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 } } diff --git a/Sources/Classes/Views/CustomizableButton/CustomizableButtonViewModel.swift b/Sources/Classes/Views/CustomizableButton/CustomizableButtonViewModel.swift index bcd6e230..b96d137f 100644 --- a/Sources/Classes/Views/CustomizableButton/CustomizableButtonViewModel.swift +++ b/Sources/Classes/Views/CustomizableButton/CustomizableButtonViewModel.swift @@ -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 } diff --git a/Sources/Extensions/DataLoading/PaginationDataLoading/PaginationWrapperUIDelegate+DefaultImplementation.swift b/Sources/Extensions/DataLoading/PaginationDataLoading/PaginationWrapperUIDelegate+DefaultImplementation.swift index a46f000a..74819c6b 100644 --- a/Sources/Extensions/DataLoading/PaginationDataLoading/PaginationWrapperUIDelegate+DefaultImplementation.swift +++ b/Sources/Extensions/DataLoading/PaginationDataLoading/PaginationWrapperUIDelegate+DefaultImplementation.swift @@ -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 } } diff --git a/Sources/Extensions/UIKit/UIButton/UIButton+ButtonHolder.swift b/Sources/Extensions/UIKit/UIButton/UIButton+ButtonHolder.swift new file mode 100644 index 00000000..cc124ddb --- /dev/null +++ b/Sources/Extensions/UIKit/UIButton/UIButton+ButtonHolder.swift @@ -0,0 +1,29 @@ +// +// Copyright (c) 2020 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import UIKit.UIButton + +extension UIButton: ButtonHolder { + public var button: UIButton { + self + } +} diff --git a/Sources/Protocols/UIKit/ButtonHolder/ButtonHolder.swift b/Sources/Protocols/UIKit/ButtonHolder/ButtonHolder.swift new file mode 100644 index 00000000..0f3b4120 --- /dev/null +++ b/Sources/Protocols/UIKit/ButtonHolder/ButtonHolder.swift @@ -0,0 +1,30 @@ +// +// Copyright (c) 2020 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import UIKit.UIButton + +/// Protocol that contains button property. +public protocol ButtonHolder { + + /// Contained UIButton instance. + var button: UIButton { get } +} diff --git a/Sources/Protocols/UIKit/ButtonHolder/ButtonHolderView.swift b/Sources/Protocols/UIKit/ButtonHolder/ButtonHolderView.swift new file mode 100644 index 00000000..22b08e84 --- /dev/null +++ b/Sources/Protocols/UIKit/ButtonHolder/ButtonHolderView.swift @@ -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 diff --git a/Sources/Structures/DataLoading/PaginationDataLoading/PaginationWrapperUIDelegate.swift b/Sources/Structures/DataLoading/PaginationDataLoading/PaginationWrapperUIDelegate.swift index 039eb2a4..2d004320 100644 --- a/Sources/Structures/DataLoading/PaginationDataLoading/PaginationWrapperUIDelegate.swift +++ b/Sources/Structures/DataLoading/PaginationDataLoading/PaginationWrapperUIDelegate.swift @@ -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() } diff --git a/TISwiftUtils/README.md b/TISwiftUtils/README.md new file mode 100644 index 00000000..d135d3e7 --- /dev/null +++ b/TISwiftUtils/README.md @@ -0,0 +1,7 @@ +# TISwiftUtils + +Bunch of useful helpers for development. + +# Installation via SPM + +You can install this framework as a target of LeadKit. diff --git a/TISwiftUtils/Sources/Extensions/Optional/Optional+Extensions.swift b/TISwiftUtils/Sources/Extensions/Optional/Optional+Extensions.swift new file mode 100644 index 00000000..0bddd3bb --- /dev/null +++ b/TISwiftUtils/Sources/Extensions/Optional/Optional+Extensions.swift @@ -0,0 +1,29 @@ +// +// Copyright (c) 2020 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import UIKit + +public extension Optional where Wrapped == String { + var orEmpty: String { + self ?? "" + } +} diff --git a/TISwiftUtils/Sources/Extensions/Substring/Substring+Extensions.swift b/TISwiftUtils/Sources/Extensions/Substring/Substring+Extensions.swift new file mode 100644 index 00000000..61d0812f --- /dev/null +++ b/TISwiftUtils/Sources/Extensions/Substring/Substring+Extensions.swift @@ -0,0 +1,30 @@ +// +// Copyright (c) 2020 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +public extension Substring { + var string: String { + String(self) + } +} + diff --git a/TISwiftUtils/Sources/Helpers/Typealias.swift b/TISwiftUtils/Sources/Helpers/Typealias.swift new file mode 100644 index 00000000..897d1f09 --- /dev/null +++ b/TISwiftUtils/Sources/Helpers/Typealias.swift @@ -0,0 +1,51 @@ +// +// Copyright (c) 2020 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import UIKit + +/// Closure with custom arguments and return value. +public typealias Closure = (Input) -> Output + +/// Closure with no arguments and custom return value. +public typealias ResultClosure = () -> Output + +/// Closure that takes custom arguments and returns Void. +public typealias ParameterClosure = Closure + +// MARK: Throwable versions + +/// Closure with custom arguments and return value, may throw an error. +public typealias ThrowableClosure = (Input) throws -> Output + +/// Closure with no arguments and custom return value, may throw an error. +public typealias ThrowableResultClosure = () throws -> Output + +/// Closure that takes custom arguments and returns Void, may throw an error. +public typealias ThrowableParameterClosure = ThrowableClosure + +// MARK: Concrete closures + +/// Closure that takes no arguments and returns Void. +public typealias VoidClosure = ResultClosure + +/// Closure that takes no arguments, may throw an error and returns Void. +public typealias ThrowableVoidClosure = () throws -> Void diff --git a/TITransitions/Assets/panel_transition.gif b/TITransitions/Assets/panel_transition.gif new file mode 100644 index 00000000..cd1a59ff Binary files /dev/null and b/TITransitions/Assets/panel_transition.gif differ diff --git a/TITransitions/README.md b/TITransitions/README.md new file mode 100644 index 00000000..aa170793 --- /dev/null +++ b/TITransitions/README.md @@ -0,0 +1,60 @@ +# TITransitions + +![Version](https://img.shields.io/github/v/release/Loupehope/TITransitions) +![Platform](https://img.shields.io/badge/platform-iOS-green) +![License](https://img.shields.io/hexpm/l/plug?color=darkBlue) + +Set of custom transitions to present controller. + +# PanelTransition +Use to present ViewController from the bottom. + +## Usage +```swift +let panelTransition = PanelTransition(presentStyle: .halfScreen) + +let childController = UIViewController() +childController.view.backgroundColor = .white + +childController.transitioningDelegate = panelTransition +childController.modalPresentationStyle = .custom + +rootController.present(childController) +``` +

+ +

+ +## 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. diff --git a/TITransitions/Sources/Base/Animation/BaseAnimation.swift b/TITransitions/Sources/Base/Animation/BaseAnimation.swift new file mode 100644 index 00000000..457eb7dc --- /dev/null +++ b/TITransitions/Sources/Base/Animation/BaseAnimation.swift @@ -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) + } +} diff --git a/TITransitions/Sources/Base/Protocol/Animation.swift b/TITransitions/Sources/Base/Protocol/Animation.swift new file mode 100644 index 00000000..fb234ef5 --- /dev/null +++ b/TITransitions/Sources/Base/Protocol/Animation.swift @@ -0,0 +1,29 @@ +// +// Copyright (c) 2020 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import UIKit + +public protocol Animation { + var duration: TimeInterval { get } + + func animator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating +} diff --git a/TITransitions/Sources/Helpers/Typealias.swift b/TITransitions/Sources/Helpers/Typealias.swift new file mode 100644 index 00000000..ccc4f580 --- /dev/null +++ b/TITransitions/Sources/Helpers/Typealias.swift @@ -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) diff --git a/TITransitions/Sources/PanelTransition/Animations/DismissAnimation.swift b/TITransitions/Sources/PanelTransition/Animations/DismissAnimation.swift new file mode 100644 index 00000000..b076134e --- /dev/null +++ b/TITransitions/Sources/PanelTransition/Animations/DismissAnimation.swift @@ -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 + } +} diff --git a/TITransitions/Sources/PanelTransition/Animations/PresentAnimation.swift b/TITransitions/Sources/PanelTransition/Animations/PresentAnimation.swift new file mode 100644 index 00000000..58d21efa --- /dev/null +++ b/TITransitions/Sources/PanelTransition/Animations/PresentAnimation.swift @@ -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 + } +} + diff --git a/TITransitions/Sources/PanelTransition/Controllers/PanelPresentationController.swift b/TITransitions/Sources/PanelTransition/Controllers/PanelPresentationController.swift new file mode 100644 index 00000000..d35b6ea6 --- /dev/null +++ b/TITransitions/Sources/PanelTransition/Controllers/PanelPresentationController.swift @@ -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() + } +} diff --git a/TITransitions/Sources/PanelTransition/Controllers/PresentStyle.swift b/TITransitions/Sources/PanelTransition/Controllers/PresentStyle.swift new file mode 100644 index 00000000..8d7fb290 --- /dev/null +++ b/TITransitions/Sources/PanelTransition/Controllers/PresentStyle.swift @@ -0,0 +1,30 @@ +// +// Copyright (c) 2020 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import UIKit + +public enum PresentStyle { + case fullScreen + case halfScreen + case customInsets(UIEdgeInsets) + case customHeight(CGFloat) +} diff --git a/TITransitions/Sources/PanelTransition/Controllers/PresentationController.swift b/TITransitions/Sources/PanelTransition/Controllers/PresentationController.swift new file mode 100644 index 00000000..ba1ae520 --- /dev/null +++ b/TITransitions/Sources/PanelTransition/Controllers/PresentationController.swift @@ -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) + } +} diff --git a/TITransitions/Sources/PanelTransition/Driver/CGPoint+Velocity.swift b/TITransitions/Sources/PanelTransition/Driver/CGPoint+Velocity.swift new file mode 100644 index 00000000..89dcb0df --- /dev/null +++ b/TITransitions/Sources/PanelTransition/Driver/CGPoint+Velocity.swift @@ -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) + } +} diff --git a/TITransitions/Sources/PanelTransition/Driver/TransitionDirection.swift b/TITransitions/Sources/PanelTransition/Driver/TransitionDirection.swift new file mode 100644 index 00000000..5856178f --- /dev/null +++ b/TITransitions/Sources/PanelTransition/Driver/TransitionDirection.swift @@ -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 +} diff --git a/TITransitions/Sources/PanelTransition/Driver/TransitionDriver.swift b/TITransitions/Sources/PanelTransition/Driver/TransitionDriver.swift new file mode 100644 index 00000000..5cbc6f62 --- /dev/null +++ b/TITransitions/Sources/PanelTransition/Driver/TransitionDriver.swift @@ -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 + } +} diff --git a/TITransitions/Sources/PanelTransition/Transition/PanelTransition.swift b/TITransitions/Sources/PanelTransition/Transition/PanelTransition.swift new file mode 100644 index 00000000..3ee010ce --- /dev/null +++ b/TITransitions/Sources/PanelTransition/Transition/PanelTransition.swift @@ -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 + } +} diff --git a/TIUIElements/README.md b/TIUIElements/README.md new file mode 100644 index 00000000..50d4e5af --- /dev/null +++ b/TIUIElements/README.md @@ -0,0 +1,7 @@ +# TIUIElements + +Bunch of useful protocols and views. + +# Installation via SPM + +You can install this framework as a target of LeadKit. diff --git a/TIUIElements/Sources/Extensions/UIActivityIndicatorView/UIActivityIndicatorView+ActivityIndicatorHolder.swift b/TIUIElements/Sources/Extensions/UIActivityIndicatorView/UIActivityIndicatorView+ActivityIndicatorHolder.swift new file mode 100644 index 00000000..b24434a4 --- /dev/null +++ b/TIUIElements/Sources/Extensions/UIActivityIndicatorView/UIActivityIndicatorView+ActivityIndicatorHolder.swift @@ -0,0 +1,30 @@ +// +// Copyright (c) 2020 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import UIKit +import TIUIKitCore + +extension UIActivityIndicatorView: ActivityIndicatorHolder { + public var activityIndicator: Animatable { + self + } +} diff --git a/TIUIElements/Sources/Extensions/UIActivityIndicatorView/UIActivityIndicatorView+Animatable.swift b/TIUIElements/Sources/Extensions/UIActivityIndicatorView/UIActivityIndicatorView+Animatable.swift new file mode 100644 index 00000000..2d709284 --- /dev/null +++ b/TIUIElements/Sources/Extensions/UIActivityIndicatorView/UIActivityIndicatorView+Animatable.swift @@ -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 {} diff --git a/TIUIKitCore/README.md b/TIUIKitCore/README.md new file mode 100644 index 00000000..9c46b9b3 --- /dev/null +++ b/TIUIKitCore/README.md @@ -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. diff --git a/TIUIKitCore/Sources/Extensions/InitializableView/InitializableView+Extensions.swift b/TIUIKitCore/Sources/Extensions/InitializableView/InitializableView+Extensions.swift new file mode 100644 index 00000000..b5026ee6 --- /dev/null +++ b/TIUIKitCore/Sources/Extensions/InitializableView/InitializableView+Extensions.swift @@ -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() + } +} diff --git a/TIUIKitCore/Sources/Extensions/UIStackView/UIStackView+Extensions.swift b/TIUIKitCore/Sources/Extensions/UIStackView/UIStackView+Extensions.swift new file mode 100644 index 00000000..9982ca65 --- /dev/null +++ b/TIUIKitCore/Sources/Extensions/UIStackView/UIStackView+Extensions.swift @@ -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) } + } +} diff --git a/TIUIKitCore/Sources/Extensions/UIView/UIView+ActivityIndicatorHolder.swift b/TIUIKitCore/Sources/Extensions/UIView/UIView+ActivityIndicatorHolder.swift new file mode 100644 index 00000000..16c6baf4 --- /dev/null +++ b/TIUIKitCore/Sources/Extensions/UIView/UIView+ActivityIndicatorHolder.swift @@ -0,0 +1,29 @@ +// +// Copyright (c) 2020 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import UIKit + +public extension ActivityIndicatorHolder where Self: UIView { + var indicatorOwner: UIView { + self + } +} diff --git a/TIUIKitCore/Sources/Extensions/UIView/UIView+Extensions.swift b/TIUIKitCore/Sources/Extensions/UIView/UIView+Extensions.swift new file mode 100644 index 00000000..c8c3877a --- /dev/null +++ b/TIUIKitCore/Sources/Extensions/UIView/UIView+Extensions.swift @@ -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) } + } +} diff --git a/TIUIKitCore/Sources/Protocols/ActivityIndicator/ActivityIndicator.swift b/TIUIKitCore/Sources/Protocols/ActivityIndicator/ActivityIndicator.swift new file mode 100644 index 00000000..6d289364 --- /dev/null +++ b/TIUIKitCore/Sources/Protocols/ActivityIndicator/ActivityIndicator.swift @@ -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 diff --git a/TIUIKitCore/Sources/Protocols/ActivityIndicator/ActivityIndicatorHolder.swift b/TIUIKitCore/Sources/Protocols/ActivityIndicator/ActivityIndicatorHolder.swift new file mode 100644 index 00000000..dffd4b32 --- /dev/null +++ b/TIUIKitCore/Sources/Protocols/ActivityIndicator/ActivityIndicatorHolder.swift @@ -0,0 +1,29 @@ +// +// Copyright (c) 2020 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import UIKit + +/// Protocol that describes placeholder view, containing activity indicator. +public protocol ActivityIndicatorHolder: class { + var activityIndicator: Animatable { get } + var indicatorOwner: UIView { get } +} diff --git a/TIUIKitCore/Sources/Protocols/Animatable/Animatable.swift b/TIUIKitCore/Sources/Protocols/Animatable/Animatable.swift new file mode 100644 index 00000000..927884dd --- /dev/null +++ b/TIUIKitCore/Sources/Protocols/Animatable/Animatable.swift @@ -0,0 +1,30 @@ +// +// Copyright (c) 2020 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +/// 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() +} diff --git a/TIUIKitCore/Sources/Protocols/InitializableView/InitializableView.swift b/TIUIKitCore/Sources/Protocols/InitializableView/InitializableView.swift new file mode 100644 index 00000000..bbf07d29 --- /dev/null +++ b/TIUIKitCore/Sources/Protocols/InitializableView/InitializableView.swift @@ -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() +} diff --git a/TIUIKitCore/Sources/Views/BaseInitializableControl.swift b/TIUIKitCore/Sources/Views/BaseInitializableControl.swift new file mode 100644 index 00000000..c5cc67f0 --- /dev/null +++ b/TIUIKitCore/Sources/Views/BaseInitializableControl.swift @@ -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 + } +} diff --git a/TIUIKitCore/Sources/Views/BaseInitializableView.swift b/TIUIKitCore/Sources/Views/BaseInitializableView.swift new file mode 100644 index 00000000..1fb7d960 --- /dev/null +++ b/TIUIKitCore/Sources/Views/BaseInitializableView.swift @@ -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 + } +}