diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b9eb707..9516a9d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +### 1.51.0 + +- **Added**: `BaseModalViewController` implementing `PanModalPresentable` with additional functionality +- **Added**: `BaseModalWrapperViewController` for wrapping `UIViewController`s with `BaseModalViewController` functionality + ### 1.50.0 - **Updated**: Fix activity indicator positioning for `StatefulButton` on iOS 15+ and disabled state touch handling diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..f9807505 --- /dev/null +++ b/Makefile @@ -0,0 +1,103 @@ +export SRCROOT := $(shell pwd) + +push_to_podspecs: TISwiftUtils.target TIFoundationUtils.target TICoreGraphicsUtils.target TIKeychainUtils.target TIUIKitCore.target TIUIElements.target TIWebView.target TIBottomSheet.target TISwiftUICore.target TITableKitUtils.target TIDeeplink.target TIDeveloperUtils.target TILogging.target TINetworking.target TIMoyaNetworking.target TINetworkingCache.target TIMapUtils.target TIAppleMapUtils.target TIGoogleMapUtils.target TIPagination.target TIAuth.target TIEcommerce.target TITextProcessing.target + $(call clean) + +TISwiftUtils.target: + MODULE_NAME="TISwiftUtils" ./project-scripts/push_to_podspecs.sh + touch TISwiftUtils.target + +TIFoundationUtils.target: TISwiftUtils.target + MODULE_NAME="TIFoundationUtils" ./project-scripts/push_to_podspecs.sh + touch TIFoundationUtils.target + +TICoreGraphicsUtils.target: + MODULE_NAME="TICoreGraphicsUtils" ./project-scripts/push_to_podspecs.sh + touch TICoreGraphicsUtils.target + +TIKeychainUtils.target: TIFoundationUtils.target + MODULE_NAME="TIKeychainUtils" ./project-scripts/push_to_podspecs.sh + touch TIKeychainUtils.target + +TIUIKitCore.target: TISwiftUtils.target + MODULE_NAME="TIUIKitCore" ./project-scripts/push_to_podspecs.sh + touch TIUIKitCore.target + +TIUIElements.target: TIUIKitCore.target + MODULE_NAME="TIUIElements" ./project-scripts/push_to_podspecs.sh + touch TIUIElements.target + +TIWebView.target: TIUIKitCore.target + MODULE_NAME="TIWebView" ./project-scripts/push_to_podspecs.sh + touch TIWebView.target + +TIBottomSheet.target: TIUIElements.target + MODULE_NAME="TIBottomSheet" ./project-scripts/push_to_podspecs.sh + touch TIBottomSheet.target + +TISwiftUICore.target: TIUIKitCore.target + MODULE_NAME="TISwiftUICore" ./project-scripts/push_to_podspecs.sh + touch TISwiftUICore.target + +TITableKitUtils.target: TIUIElements.target + MODULE_NAME="TITableKitUtils" ./project-scripts/push_to_podspecs.sh + touch TITableKitUtils.target + +TIDeeplink.target: TIFoundationUtils.target + MODULE_NAME="TIDeeplink" ./project-scripts/push_to_podspecs.sh + touch TIDeeplink.target + +TIDeveloperUtils.target: TIUIElements.target + MODULE_NAME="TIDeveloperUtils" ./project-scripts/push_to_podspecs.sh + touch TIDeveloperUtils.target + +TINetworking.target: TIFoundationUtils.target + MODULE_NAME="TINetworking" ./project-scripts/push_to_podspecs.sh + touch TINetworking.target + +TILogging.target: + MODULE_NAME="TILogging" ./project-scripts/push_to_podspecs.sh + touch TILogging.target + +TIMoyaNetworking.target: TINetworking.target + MODULE_NAME="TIMoyaNetworking" ./project-scripts/push_to_podspecs.sh + touch TIMoyaNetworking.target + +TINetworkingCache.target: TINetworking.target + MODULE_NAME="TINetworkingCache" ./project-scripts/push_to_podspecs.sh + touch TINetworkingCache.target + +TIMapUtils.target: TILogging TICoreGraphicsUtils.target + MODULE_NAME="TIMapUtils" ./project-scripts/push_to_podspecs.sh + touch TIMapUtils.target + +TIAppleMapUtils.target: TIMapUtils.target + MODULE_NAME="TIAppleMapUtils" ./project-scripts/push_to_podspecs.sh + touch TIAppleMapUtils.target + +TIGoogleMapUtils.target: TIMapUtils.target + MODULE_NAME="TIGoogleMapUtils" ./project-scripts/push_to_podspecs.sh + touch TIGoogleMapUtils.target + +TIYandexMapUtils.target: TIMapUtils.target + MODULE_NAME="TIYandexMapUtils" ./project-scripts/push_to_podspecs.sh + touch TIYandexMapUtils.target + +TIPagination.target: TISwiftUtils.target + MODULE_NAME="TIPagination" ./project-scripts/push_to_podspecs.sh + touch TIPagination.target + +TIAuth.target: TIUIKitCore.target TIKeychainUtils.target + MODULE_NAME="TIAuth" ./project-scripts/push_to_podspecs.sh + touch TIAuth.target + +TIEcommerce.target: TINetworking.target TIUIElements.target + MODULE_NAME="TIEcommerce" ./project-scripts/push_to_podspecs.sh + touch TIEcommerce.target + +TITextProcessing.target: + MODULE_NAME="TITextProcessing" ./project-scripts/push_to_podspecs.sh + touch TITextProcessing.target + +clean: + rm *.target diff --git a/Package.resolved b/Package.resolved index 810d97dc..ae273492 100644 --- a/Package.resolved +++ b/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Alamofire/Alamofire.git", "state" : { - "revision" : "f96b619bcb2383b43d898402283924b80e2c4bae", - "version" : "5.4.3" + "revision" : "bc268c28fb170f494de9e9927c371b8342979ece", + "version" : "5.7.1" } }, { @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/petropavel13/Cursors", "state" : { - "revision" : "a1561869135e72832eff3b1e729075c56c2eebf6", - "version" : "0.5.1" + "revision" : "52f27b82cb1cbbc2b5fd09514c48b9c75e3b0300", + "version" : "0.6.0" } }, { @@ -50,8 +50,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Moya/Moya.git", "state" : { - "revision" : "9b906860e3c3c09032879465c471e6375829593f", - "version" : "15.0.0" + "revision" : "c263811c1f3dbf002be9bd83107f7cdc38992b26", + "version" : "15.0.3" + } + }, + { + "identity" : "panmodal", + "kind" : "remoteSourceControl", + "location" : "https://git.svc.touchin.ru/TouchInstinct/PanModal", + "state" : { + "revision" : "ced7c1703f90746df0224b6e0d33c146d9ae4284", + "version" : "1.3.1" } }, { @@ -68,8 +77,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/ReactiveX/RxSwift.git", "state" : { - "revision" : "b4307ba0b6425c0ba4178e138799946c3da594f8", - "version" : "6.5.0" + "revision" : "9dcaa4b333db437b0fbfaf453fad29069044a8b4", + "version" : "6.6.0" } }, { diff --git a/Package.swift b/Package.swift index b61b97ef..feca2418 100644 --- a/Package.swift +++ b/Package.swift @@ -15,6 +15,7 @@ let package = Package( .library(name: "TIUIKitCore", targets: ["TIUIKitCore"]), .library(name: "TIUIElements", targets: ["TIUIElements"]), .library(name: "TIWebView", targets: ["TIWebView"]), + .library(name: "TIBottomSheet", targets: ["TIBottomSheet"]), // MARK: - SwiftUI .library(name: "TISwiftUICore", targets: ["TISwiftUICore"]), @@ -54,7 +55,8 @@ let package = Package( .package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.4.0")), .package(url: "https://github.com/Moya/Moya.git", .upToNextMajor(from: "15.0.0")), .package(url: "https://github.com/hyperoslo/Cache.git", .upToNextMajor(from: "6.0.0")), - .package(url: "https://github.com/antlr/antlr4", .upToNextMinor(from: "4.10.1")) + .package(url: "https://github.com/antlr/antlr4", .upToNextMinor(from: "4.10.1")), + .package(url: "https://git.svc.touchin.ru/TouchInstinct/PanModal", .upToNextMinor(from: "1.3.0")) ], targets: [ @@ -68,9 +70,16 @@ let package = Package( plugins: [.plugin(name: "TISwiftLintPlugin")]), .target(name: "TIWebView", dependencies: ["TIUIKitCore", "TISwiftUtils"], path: "TIWebView/Sources"), + .target(name: "TIBottomSheet", + dependencies: ["PanModal", "TIUIElements", "TIUIKitCore", "TISwiftUtils"], + path: "TIBottomSheet/Sources", + exclude: ["../TIBottomSheet.app"], + plugins: [.plugin(name: "TISwiftLintPlugin")]), // MARK: - SwiftUI - .target(name: "TISwiftUICore", dependencies: ["TIUIKitCore", "TISwiftUtils"], path: "TISwiftUICore/Sources"), + .target(name: "TISwiftUICore", + dependencies: ["TIUIKitCore", "TISwiftUtils"], + path: "TISwiftUICore/Sources"), // MARK: - Utils @@ -109,12 +118,12 @@ let package = Package( plugins: [.plugin(name: "TISwiftLintPlugin")]), .target(name: "TIMoyaNetworking", - dependencies: ["TINetworking", "TIFoundationUtils", "Moya"], + dependencies: ["TINetworking", "Moya"], path: "TIMoyaNetworking/Sources", plugins: [.plugin(name: "TISwiftLintPlugin")]), .target(name: "TINetworkingCache", - dependencies: ["TIFoundationUtils", "TINetworking", "Cache"], + dependencies: ["TINetworking", "Cache"], path: "TINetworkingCache/Sources", plugins: [.plugin(name: "TISwiftLintPlugin")]), @@ -134,7 +143,7 @@ let package = Package( .target(name: "OTPSwiftView", dependencies: ["TIUIElements"], path: "OTPSwiftView/Sources"), .target(name: "TITransitions", path: "TITransitions/Sources"), .target(name: "TIPagination", dependencies: ["Cursors", "TISwiftUtils"], path: "TIPagination/Sources"), - .target(name: "TIAuth", dependencies: ["TIFoundationUtils", "TIUIKitCore", "TIKeychainUtils"], path: "TIAuth/Sources"), + .target(name: "TIAuth", dependencies: ["TIUIKitCore", "TIKeychainUtils"], path: "TIAuth/Sources"), .target(name: "TIEcommerce", dependencies: ["TIFoundationUtils", "TISwiftUtils", "TINetworking", "TIUIKitCore", "TIUIElements"], path: "TIEcommerce/Sources"), .target(name: "TITextProcessing", dependencies: [.product(name: "Antlr4", package: "antlr4")], diff --git a/README.md b/README.md index 72009c64..955e78ce 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,7 @@ LICENSE - [TITextProcessing](docs/titextprocessing) * [TITextProcessing](docs/titextprocessing/titextprocessing.md) - [TIDeeplink](docs/tideeplink/deeplinks.md) +- [TIBottomSheet](docs/tibottomsheet/tibottomsheet.md) - [Semantic Commit Messages](docs/semantic-commit-messages.md) - commit message codestyle. - [Snippets](docs/snippets.md) - useful commands and scripts for development. diff --git a/TIAppleMapUtils/TIAppleMapUtils.podspec b/TIAppleMapUtils/TIAppleMapUtils.podspec index 05398773..1319b561 100644 --- a/TIAppleMapUtils/TIAppleMapUtils.podspec +++ b/TIAppleMapUtils/TIAppleMapUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIAppleMapUtils' - s.version = '1.50.0' + s.version = '1.51.0' s.summary = 'Set of helpers for map objects clustering and interacting using Apple MapKit.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIAuth/TIAuth.podspec b/TIAuth/TIAuth.podspec index 6f585a37..231c1c91 100644 --- a/TIAuth/TIAuth.podspec +++ b/TIAuth/TIAuth.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIAuth' - s.version = '1.50.0' + s.version = '1.51.0' s.summary = 'Login, registration, confirmation and other related actions' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIBottomSheet/PlaygroundPodfile b/TIBottomSheet/PlaygroundPodfile new file mode 100644 index 00000000..beb2a489 --- /dev/null +++ b/TIBottomSheet/PlaygroundPodfile @@ -0,0 +1,13 @@ +ENV["DEVELOPMENT_INSTALL"] = "true" + +source 'https://git.svc.touchin.ru/TouchInstinct/Podspecs.git' + +target 'TIBottomSheet' do + platform :ios, 11 + use_frameworks! + + pod 'TIUIElements', :path => '../../../../TIUIElements/TIUIElements.podspec' + pod 'TIUIKitCore', :path => '../../../../TIUIKitCore/TIUIKitCore.podspec' + pod 'TISwiftUtils', :path => '../../../../TISwiftUtils/TISwiftUtils.podspec' + pod 'TIBottomSheet', :path => '../../../../TIBottomSheet/TIBottomSheet.podspec' +end diff --git a/TIBottomSheet/Sources/BottomSheet/BaseModalViewController+Appearance.swift b/TIBottomSheet/Sources/BottomSheet/BaseModalViewController+Appearance.swift new file mode 100644 index 00000000..53a4002e --- /dev/null +++ b/TIBottomSheet/Sources/BottomSheet/BaseModalViewController+Appearance.swift @@ -0,0 +1,62 @@ +// +// Copyright (c) 2023 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 TIUIElements +import TIUIKitCore +import UIKit + +extension BaseModalViewController { + + open class BaseAppearance: UIView.BaseAppearance { + + public var presentationDetents: [ModalViewPresentationDetent] + public var dragViewState: DragView.State + public var headerViewState: ModalHeaderView.State + public var footerViewState: ModalFooterView.State + + public init(layout: UIView.DefaultWrappedLayout = .defaultLayout, + backgroundColor: UIColor = .clear, + border: UIViewBorder = .init(), + shadow: UIViewShadow? = nil, + presentationDetents: [ModalViewPresentationDetent] = [.maxHeight], + dragViewState: DragView.State = .hidden, + headerViewState: ModalHeaderView.State = .hidden, + footerViewState: ModalFooterView.State = .hidden) { + + self.presentationDetents = presentationDetents + self.dragViewState = dragViewState + self.headerViewState = headerViewState + self.footerViewState = footerViewState + + super.init(layout: layout, + backgroundColor: backgroundColor, + border: border, + shadow: shadow) + } + } + + public final class DefaultAppearance: BaseAppearance, ViewAppearance { + public static var defaultAppearance: Self { + Self() + } + } +} diff --git a/TIBottomSheet/Sources/BottomSheet/BaseModalViewController.swift b/TIBottomSheet/Sources/BottomSheet/BaseModalViewController.swift new file mode 100644 index 00000000..8f340340 --- /dev/null +++ b/TIBottomSheet/Sources/BottomSheet/BaseModalViewController.swift @@ -0,0 +1,473 @@ +// +// Copyright (c) 2023 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 TISwiftUtils +import TIUIElements +import TIUIKitCore +import UIKit +import PanModal + +open class BaseModalViewController: BaseInitializableViewController, PanModalPresentable { + + // MARK: - Public Properties + + public let dragView = DragView() + public let headerView = ModalHeaderView() + private(set) public lazy var contentView = createContentView() + private(set) public lazy var footerView = createFooterView() + + public private(set) lazy var dragViewBottomToHeaderViewTopConstraint: NSLayoutConstraint = { + dragView.bottomAnchor.constraint(equalTo: headerView.topAnchor) + }() + + public private(set) lazy var dragViewBottomToContentViewTopConstraint: NSLayoutConstraint = { + dragView.bottomAnchor.constraint(equalTo: contentView.topAnchor) + }() + + public private(set) lazy var dragViewConstraints: SubviewConstraints = { + let trailingConstraint = dragView.trailingAnchor.constraint(equalTo: view.trailingAnchor) + + let edgeConstraints = EdgeConstraints(leadingConstraint: dragView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + trailingConstraint: trailingConstraint, + topConstraint: dragView.topAnchor.constraint(equalTo: view.topAnchor), + bottomConstraint: dragViewBottomToHeaderViewTopConstraint) + + let centerXConstraint = dragView.centerXAnchor.constraint(equalTo: view.centerXAnchor) + let centerYConstraint = dragView.centerYAnchor.constraint(equalTo: view.centerYAnchor) + + let centerConstraints = CenterConstraints(centerXConstraint: centerXConstraint, + centerYConstraint: centerYConstraint) + + let sizeConstraints = SizeConstraints(widthConstraint: dragView.widthAnchor.constraint(equalToConstant: .zero), + heightConstraint: dragView.heightAnchor.constraint(equalToConstant: .zero)) + + return SubviewConstraints(edgeConstraints: edgeConstraints, + centerConstraints: centerConstraints, + sizeConstraints: sizeConstraints) + }() + + public private(set) lazy var headerViewToSuperviewTopConstraint: NSLayoutConstraint = { + headerView.topAnchor.constraint(equalTo: view.topAnchor) + }() + + public private(set) lazy var headerBottomToContentTopConstraint: NSLayoutConstraint = { + headerView.bottomAnchor.constraint(equalTo: contentView.topAnchor) + }() + + public private(set) lazy var headerViewConstraints: SubviewConstraints = { + let leadingConstraint = headerView.leadingAnchor.constraint(equalTo: view.leadingAnchor) + let trailingConstraint = headerView.trailingAnchor.constraint(equalTo: view.trailingAnchor) + + let edgeConstraints = EdgeConstraints(leadingConstraint: leadingConstraint, + trailingConstraint: trailingConstraint, + topConstraint: dragViewBottomToHeaderViewTopConstraint, + bottomConstraint: headerBottomToContentTopConstraint) + + let centerXConstraint = headerView.centerXAnchor.constraint(equalTo: view.centerXAnchor) + let centerYConstraint = headerView.centerYAnchor.constraint(equalTo: view.centerYAnchor) + + let centerConstraints = CenterConstraints(centerXConstraint: centerXConstraint, + centerYConstraint: centerYConstraint) + + let sizeConstraints = SizeConstraints(widthConstraint: headerView.widthAnchor.constraint(equalToConstant: .zero), + heightConstraint: headerView.heightAnchor.constraint(equalToConstant: .zero)) + + return SubviewConstraints(edgeConstraints: edgeConstraints, + centerConstraints: centerConstraints, + sizeConstraints: sizeConstraints) + }() + + public private(set) lazy var contentViewTopToSuperviewConstraint: NSLayoutConstraint = { + contentView.topAnchor.constraint(equalTo: view.topAnchor) + }() + + public private(set) lazy var contentViewBottomToSuperviewConstraint: NSLayoutConstraint = { + contentView.bottomAnchor.constraint(equalTo: view.bottomAnchor) + }() + + public private(set) lazy var contentViewConstraints: SubviewConstraints = { + let leadingConstraint = contentView.leadingAnchor.constraint(equalTo: view.leadingAnchor) + let trailingConstraint = contentView.trailingAnchor.constraint(equalTo: view.trailingAnchor) + + let edgeConstraints = EdgeConstraints(leadingConstraint: leadingConstraint, + trailingConstraint: trailingConstraint, + topConstraint: headerBottomToContentTopConstraint, + bottomConstraint: contentViewBottomToSuperviewConstraint) + + let centerXConstraint = contentView.centerXAnchor.constraint(equalTo: view.centerXAnchor) + let centerYConstraint = contentView.centerYAnchor.constraint(equalTo: view.centerYAnchor) + + let centerConstraints = CenterConstraints(centerXConstraint: centerXConstraint, + centerYConstraint: centerYConstraint) + + let sizeConstraints = SizeConstraints(widthConstraint: contentView.widthAnchor.constraint(equalToConstant: .zero), + heightConstraint: contentView.heightAnchor.constraint(equalToConstant: .zero)) + + return SubviewConstraints(edgeConstraints: edgeConstraints, + centerConstraints: centerConstraints, + sizeConstraints: sizeConstraints) + }() + + public private(set) lazy var footerViewConstraints: SubviewConstraints = { + let leadingConstraint = footerView.leadingAnchor.constraint(equalTo: view.leadingAnchor) + let trailingConstraint = footerView.trailingAnchor.constraint(equalTo: view.trailingAnchor) + + let edgeConstraints = EdgeConstraints(leadingConstraint: leadingConstraint, + trailingConstraint: trailingConstraint, + topConstraint: footerView.topAnchor.constraint(equalTo: contentView.bottomAnchor), + bottomConstraint: footerView.bottomAnchor.constraint(equalTo: view.bottomAnchor)) + + let centerXConstraint = footerView.centerXAnchor.constraint(equalTo: view.centerXAnchor) + let centerYConstraint = footerView.centerYAnchor.constraint(equalTo: view.centerYAnchor) + + let centerConstraints = CenterConstraints(centerXConstraint: centerXConstraint, + centerYConstraint: centerYConstraint) + + let sizeConstraints = SizeConstraints(widthConstraint: footerView.widthAnchor.constraint(equalToConstant: .zero), + heightConstraint: footerView.heightAnchor.constraint(equalToConstant: .zero)) + + return SubviewConstraints(edgeConstraints: edgeConstraints, + centerConstraints: centerConstraints, + sizeConstraints: sizeConstraints) + }() + + // MARK: - Modal View Controller Configuration + + public var viewControllerAppearance: BaseAppearance = .init(backgroundColor: .white) + + open var panScrollable: UIScrollView? { + contentView as? UIScrollView + } + + public var panScrollableInsets: UIEdgeInsets = .zero { + didSet { + panScrollable?.contentInset = panScrollableInsets + } + } + + public var dimmedView = DimmedView() + + public var showDragIndicator = true + + open var headerViewHeight: CGFloat { + let dragViewHeight = getHeight(of: dragView) + let headerViewHeight = getHeight(of: headerView) + let dragViewVerticalInsets = getDragViewVerticalInsets() + let headerViewVerticalInsets = getHeaderViewVerticalInsets() + + return dragViewHeight + headerViewHeight + dragViewVerticalInsets + headerViewVerticalInsets + } + + open var longFormHeight: PanModalHeight { + let detents = getSortedDetents() + + return detents.max()?.panModalHeight(headerHeight: headerViewHeight) ?? .maxHeight + } + + open var mediumFormHeight: PanModalHeight { + let detents = getSortedDetents() + + if detents.count > 1 { + return detents[1].panModalHeight(headerHeight: headerViewHeight) + } + + return detents.first?.panModalHeight(headerHeight: headerViewHeight) ?? .maxHeight + } + + open var shortFormHeight: PanModalHeight { + let detents = getSortedDetents() + + return detents.min()?.panModalHeight(headerHeight: headerViewHeight) ?? .maxHeight + } + + private var keyboardDidShownObserver: NSObjectProtocol? + private var keyboardDidHiddenObserver: NSObjectProtocol? + + // MARK: - Life Cycle + + deinit { + let notificationCenter = NotificationCenter.default + + if let keyboardDidShownObserver { + notificationCenter.removeObserver(keyboardDidShownObserver) + } + + if let keyboardDidHiddenObserver { + notificationCenter.removeObserver(keyboardDidHiddenObserver) + } + } + + // MARK: - BaseInitializableViewController + + open override func addViews() { + super.addViews() + + view.addSubviews(dragView, headerView, contentView, footerView) + } + + open override func configureLayout() { + super.configureLayout() + + for view in [dragView, headerView, contentView, footerView] { + view.translatesAutoresizingMaskIntoConstraints = false + } + + configureDragViewLayout() + configureHeaderViewLayout() + configureContentViewLayout() + configureFooterViewLayout() + } + + open override func bindViews() { + super.bindViews() + + keyboardDidShownObserver = NotificationCenter.default + .addObserver(forName: UIResponder.keyboardDidShowNotification, + object: nil, + queue: .main) { [weak self] notification in + self?.configureLayoutForKeyboard(notification, isKeyboardHidden: false) + } + + keyboardDidHiddenObserver = NotificationCenter.default + .addObserver(forName: UIResponder.keyboardDidHideNotification, + object: nil, + queue: .main) { [weak self] notification in + self?.configureLayoutForKeyboard(notification, isKeyboardHidden: true) + } + } + + open override func configureAppearance() { + super.configureAppearance() + + view.configureUIView(appearance: viewControllerAppearance) + configureDragViewAppearance() + configureHeaderViewAppearance() + configureFooterViewAppearance() + } + + // MARK: - Open Methods + + open func createContentView() -> ContentView { + ContentView() + } + + open func createFooterView() -> ModalFooterView { + ModalFooterView() + } + + open func configureLayoutForKeyboard(_ notification: Notification, isKeyboardHidden: Bool) { + guard let keyboardHeight = getKeyboardHeight(notification) else { + return + } + + if case let .presented(footerViewAppearance) = viewControllerAppearance.footerViewState { + let bottomInset = footerViewAppearance.layout.insets.add(\.bottom, + to: \.bottom, + of: .vertical(bottom: keyboardHeight)) + + footerViewConstraints.edgeConstraints.bottomConstraint.constant = bottomInset + } else if let panScrollable { + var insets = panScrollableInsets + + if isKeyboardHidden { + panScrollable.contentInset = insets + } else { + insets.bottom += keyboardHeight + + panScrollable.contentInset = insets + } + } + } + + // MARK: - Private Methods + + private func configureDragViewLayout() { + guard case let .presented(dragViewAppearance) = viewControllerAppearance.dragViewState else { + return + } + + let bottomConstraint: NSLayoutConstraint + let bottomConstant: CGFloat + + if case let .presented(headerViewAppearance) = viewControllerAppearance.headerViewState { + dragViewBottomToContentViewTopConstraint.isActive = false + bottomConstraint = dragViewBottomToHeaderViewTopConstraint + bottomConstant = dragViewAppearance.layout.insets.add(\.bottom, + to: \.top, + of: headerViewAppearance.layout.insets) + } else { + dragViewBottomToHeaderViewTopConstraint.isActive = false + bottomConstraint = dragViewBottomToContentViewTopConstraint + bottomConstant = dragViewAppearance.layout.insets.bottom + } + + dragViewConstraints.edgeConstraints.bottomConstraint = bottomConstraint + dragViewConstraints.update(from: dragViewAppearance.layout) + + bottomConstraint.setActiveConstantOrDeactivate(constant: bottomConstant) + } + + private func configureHeaderViewLayout() { + guard case let .presented(headerViewAppearance) = viewControllerAppearance.headerViewState else { + return + } + + let topConstraint: NSLayoutConstraint + let topConstant: CGFloat + + if case let .presented(dragViewAppearance) = viewControllerAppearance.dragViewState { + dragViewBottomToContentViewTopConstraint.isActive = false + topConstraint = dragViewBottomToHeaderViewTopConstraint + + topConstant = dragViewAppearance.layout.insets.add(\.bottom, + to: \.top, + of: headerViewAppearance.layout.insets) + } else { + dragViewBottomToHeaderViewTopConstraint.isActive = false + topConstraint = headerViewToSuperviewTopConstraint + + let topInset = headerViewAppearance.layout.insets.top + topConstant = topInset.isFinite ? topInset : .zero + } + + headerViewConstraints.edgeConstraints.topConstraint = topConstraint + headerViewConstraints.update(from: headerViewAppearance.layout) + + topConstraint.setActiveConstantOrDeactivate(constant: topConstant) + } + + private func configureContentViewLayout() { + let topConstraint: NSLayoutConstraint + let topConstant: CGFloat + + if case let .presented(headerViewAppearance) = viewControllerAppearance.headerViewState { + dragViewBottomToContentViewTopConstraint.isActive = false + contentViewTopToSuperviewConstraint.isActive = false + + topConstraint = headerBottomToContentTopConstraint + topConstant = headerViewAppearance.layout.insets.bottom + } else if case let .presented(dragViewAppearance) = viewControllerAppearance.dragViewState { + contentViewTopToSuperviewConstraint.isActive = false + headerBottomToContentTopConstraint.isActive = false + + topConstraint = dragViewBottomToContentViewTopConstraint + topConstant = dragViewAppearance.layout.insets.bottom + } else { + headerBottomToContentTopConstraint.isActive = false + dragViewBottomToContentViewTopConstraint.isActive = false + + topConstraint = contentViewTopToSuperviewConstraint + topConstant = .zero + } + + contentViewConstraints.edgeConstraints.topConstraint = topConstraint + let layout = UIView.DefaultWrappedLayout(insets: .horizontal(.zero) + .vertical(top: topConstant) + .replacingNan(with: .zero)) + contentViewConstraints.update(from: layout) + } + + private func configureFooterViewLayout() { + guard case let .presented(footerViewAppearance) = viewControllerAppearance.footerViewState else { + return + } + + contentViewConstraints.edgeConstraints.bottomConstraint.isActive = false + contentViewConstraints.edgeConstraints.bottomConstraint = footerViewConstraints.edgeConstraints.topConstraint + + footerViewConstraints.update(from: footerViewAppearance.layout) + } + + private func configureDragViewAppearance() { + switch viewControllerAppearance.dragViewState { + case .hidden: + dragView.isHidden = true + + case let .presented(appearance): + dragView.configure(appearance: appearance) + } + } + + private func configureHeaderViewAppearance() { + switch viewControllerAppearance.headerViewState { + case .hidden: + headerView.isHidden = true + + case let .presented(appearance): + headerView.configure(appearance: appearance) + } + } + + private func configureFooterViewAppearance() { + switch viewControllerAppearance.footerViewState { + case .hidden: + footerView.isHidden = true + + case let .presented(appearance): + footerView.configureBaseWrappedViewHolder(appearance: appearance) + } + } + + private func getSortedDetents() -> [ModalViewPresentationDetent] { + viewControllerAppearance.presentationDetents.uniqued().sorted() + } + + private func getHeight(of view: UIView) -> CGFloat { + guard !view.isHidden else { + return .zero + } + + return getFittingSize(forView: view).height + } + + private func getDragViewVerticalInsets() -> CGFloat { + guard case let .presented(appearance) = viewControllerAppearance.dragViewState else { + return .zero + } + + return appearance.layout.insets.vertical(onNan: .zero) + } + + private func getHeaderViewVerticalInsets() -> CGFloat { + guard case let .presented(appearance) = viewControllerAppearance.headerViewState else { + return .zero + } + + return appearance.layout.insets.vertical(onNan: .zero) + } + + private func getFittingSize(forView view: UIView) -> CGSize { + let targetSize = CGSize(width: UIScreen.main.bounds.width, + height: UIView.layoutFittingCompressedSize.height) + + return view.systemLayoutSizeFitting(targetSize) + } + + private func getKeyboardHeight(_ notification: Notification) -> CGFloat? { + guard let userInfo = notification.userInfo else { + return nil + } + + return (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect)?.height + } +} diff --git a/TIBottomSheet/Sources/BottomSheet/DefaultModalWrapperViewController.swift b/TIBottomSheet/Sources/BottomSheet/DefaultModalWrapperViewController.swift new file mode 100644 index 00000000..b70b056a --- /dev/null +++ b/TIBottomSheet/Sources/BottomSheet/DefaultModalWrapperViewController.swift @@ -0,0 +1,54 @@ +// +// Copyright (c) 2023 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 DefaultModalWrapperViewController: BaseModalViewController { + + private(set) public var contentViewController: ContentController + + public init(contentViewController: ContentController) { + self.contentViewController = contentViewController + + super.init(nibName: nil, bundle: nil) + + addChild(contentViewController) + contentViewController.didMove(toParent: self) + } + + @available(*, unavailable) + required public init?(coder: NSCoder) { + contentViewController = ContentController() + + super.init(coder: coder) + } + + open override func createContentView() -> UIView { + contentViewController.view + } +} + +public extension UIViewController { + func wrappedInBottomSheetController() -> DefaultModalWrapperViewController { + DefaultModalWrapperViewController(contentViewController: self) + } +} diff --git a/TIBottomSheet/Sources/BottomSheet/Extension/UIViewShadow+ModalView.swift b/TIBottomSheet/Sources/BottomSheet/Extension/UIViewShadow+ModalView.swift new file mode 100644 index 00000000..611798c5 --- /dev/null +++ b/TIBottomSheet/Sources/BottomSheet/Extension/UIViewShadow+ModalView.swift @@ -0,0 +1,34 @@ +// +// Copyright (c) 2023 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 + +extension UIViewShadow { + public static var defaultModalViewShadow: Self { + .init { + Color(.black) + Opacity(0.5) + Offset(0, -5) + Radius(10) + } + } +} diff --git a/TIBottomSheet/Sources/BottomSheet/Models/ModalViewPresentationDetent.swift b/TIBottomSheet/Sources/BottomSheet/Models/ModalViewPresentationDetent.swift new file mode 100644 index 00000000..6948914a --- /dev/null +++ b/TIBottomSheet/Sources/BottomSheet/Models/ModalViewPresentationDetent.swift @@ -0,0 +1,67 @@ +// +// Copyright (c) 2022 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 +import PanModal + +public struct ModalViewPresentationDetent: Hashable { + + // MARK: - Default Values + + public static var headerOnly: Self { + Self(height: -.greatestFiniteMagnitude) + } + + public static func height(_ height: CGFloat) -> Self { + Self(height: height) + } + + public static var maxHeight: Self { + Self(height: .greatestFiniteMagnitude) + } + + // MARK: - Public Properties + + public var height: CGFloat + + // MARK: - Internal Methods + + func panModalHeight(headerHeight: CGFloat = .zero) -> PanModalHeight { + if self == .headerOnly { + return .contentHeight(headerHeight) + } + + if self == .maxHeight { + return .maxHeight + } + + return .contentHeight(height) + } +} + +// MARK: - Comparable + +extension ModalViewPresentationDetent: Comparable { + public static func < (lhs: ModalViewPresentationDetent, rhs: ModalViewPresentationDetent) -> Bool { + lhs.height < rhs.height + } +} diff --git a/TIBottomSheet/Sources/BottomSheet/Views/DragView.swift b/TIBottomSheet/Sources/BottomSheet/Views/DragView.swift new file mode 100644 index 00000000..f66b4d3a --- /dev/null +++ b/TIBottomSheet/Sources/BottomSheet/Views/DragView.swift @@ -0,0 +1,70 @@ +// +// Copyright (c) 2023 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 TIUIElements +import TIUIKitCore +import UIKit + +public final class DragView: BaseInitializableView, AppearanceConfigurable { + + // MARK: - Nested Types + + private enum Constants { + static var dragViewTopInset: CGFloat { + 8 + } + + static var dragViewBorder: UIViewBorder { + UIViewBorder(cornerRadius: dragViewSize.height / 2, roundedCorners: .allCorners) + } + + static var dragViewBackgroundColor: UIColor { + .lightGray + } + + static var dragViewSize: CGSize { + CGSize(width: 52, height: 7) + } + } + + public enum State { + case hidden + case presented(Appearance) + } + + public final class Appearance: UIView.BaseWrappedAppearance, WrappedViewAppearance { + + public static var defaultAppearance: Self { + Self(layout: DefaultWrappedLayout(insets: .vertical(top: Constants.dragViewTopInset), + size: Constants.dragViewSize, + centerOffset: .centerHorizontal()), + backgroundColor: Constants.dragViewBackgroundColor, + border: Constants.dragViewBorder) + } + } + + // MARK: - AppearanceConfigurable + + public func configure(appearance: Appearance) { + configureUIView(appearance: appearance) + } +} diff --git a/TIBottomSheet/Sources/BottomSheet/Views/ModalFooterView.swift b/TIBottomSheet/Sources/BottomSheet/Views/ModalFooterView.swift new file mode 100644 index 00000000..51e84e39 --- /dev/null +++ b/TIBottomSheet/Sources/BottomSheet/Views/ModalFooterView.swift @@ -0,0 +1,45 @@ +// +// Copyright (c) 2023 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 TIUIElements +import TIUIKitCore +import UIKit + +open class ModalFooterView: ContainerView { + // MARK: - Nested Types + + public enum State { + case hidden + case presented(Appearance) + } +} + +public extension ModalFooterView { + + final class Appearance: BaseWrappedViewHolderAppearance, + WrappedViewHolderAppearance { + public static var defaultAppearance: Self { + Self(layout: DefaultWrappedLayout(insets: .horizontal(.zero).vertical(bottom: .zero), + size: .fixedHeight(44))) + } + } +} diff --git a/TIBottomSheet/Sources/BottomSheet/Views/ModalHeaderView.swift b/TIBottomSheet/Sources/BottomSheet/Views/ModalHeaderView.swift new file mode 100644 index 00000000..815d6493 --- /dev/null +++ b/TIBottomSheet/Sources/BottomSheet/Views/ModalHeaderView.swift @@ -0,0 +1,232 @@ +// +// Copyright (c) 2023 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 TIUIElements +import TIUIKitCore +import UIKit + +open class ModalHeaderView: BaseInitializableView, AppearanceConfigurable { + + // MARK: - Nested Types + + public enum State { + case hidden + case presented(Appearance) + } + + public enum ContentViewState { + case leadingButton(BaseButtonStyle) + case trailingButton(BaseButtonStyle) + case buttons(leading: BaseButtonStyle, trailing: BaseButtonStyle) + case custom(view: UIView, appearance: UIView.BaseWrappedAppearance) + } + + // MARK: - Public properties + + public let leadingButton = StatefulButton() + public let trailingButton = StatefulButton() + + public private(set) lazy var leftTrailingToRightLeadingConstraint: NSLayoutConstraint = { + leadingButton.trailingAnchor.constraint(equalTo: trailingButton.leadingAnchor) + }() + + public private(set) lazy var leftTrailingToSuperviewTrailing: NSLayoutConstraint = { + leadingButton.trailingAnchor.constraint(equalTo: trailingAnchor) + }() + + public private(set) lazy var leadingButtonConstraints: SubviewConstraints = { + let edgeConstraints = EdgeConstraints(leadingConstraint: leadingButton.leadingAnchor.constraint(equalTo: leadingAnchor), + trailingConstraint: leftTrailingToRightLeadingConstraint, + topConstraint: leadingButton.topAnchor.constraint(equalTo: topAnchor), + bottomConstraint: leadingButton.bottomAnchor.constraint(equalTo: bottomAnchor)) + + let centerXConstraint = leadingButton.centerXAnchor.constraint(equalTo: centerXAnchor) + let centerYConstraint = leadingButton.centerYAnchor.constraint(equalTo: centerYAnchor) + + let centerConstraints = CenterConstraints(centerXConstraint: centerXConstraint, + centerYConstraint: centerYConstraint) + + let sizeConstraints = SizeConstraints(widthConstraint: leadingButton.widthAnchor.constraint(equalToConstant: .zero), + heightConstraint: leadingButton.heightAnchor.constraint(equalToConstant: .zero)) + + return SubviewConstraints(edgeConstraints: edgeConstraints, + centerConstraints: centerConstraints, + sizeConstraints: sizeConstraints) + }() + + public private(set) lazy var rightLeadingToSuperviewLeadingConstraint: NSLayoutConstraint = { + trailingButton.leadingAnchor.constraint(equalTo: leadingAnchor) + }() + + public private(set) lazy var trailingButtonConstraints: SubviewConstraints = { + let trailingConstraint = trailingButton.trailingAnchor.constraint(equalTo: trailingAnchor) + + let edgeConstraints = EdgeConstraints(leadingConstraint: leftTrailingToRightLeadingConstraint, + trailingConstraint: trailingConstraint, + topConstraint: trailingButton.topAnchor.constraint(equalTo: topAnchor), + bottomConstraint: trailingButton.bottomAnchor.constraint(equalTo: bottomAnchor)) + + let centerXConstraint = trailingButton.centerXAnchor.constraint(equalTo: centerXAnchor) + let centerYConstraint = trailingButton.centerYAnchor.constraint(equalTo: centerYAnchor) + + let centerConstraints = CenterConstraints(centerXConstraint: centerXConstraint, + centerYConstraint: centerYConstraint) + + let sizeConstraints = SizeConstraints(widthConstraint: trailingButton.widthAnchor.constraint(equalToConstant: .zero), + heightConstraint: trailingButton.heightAnchor.constraint(equalToConstant: .zero)) + + return SubviewConstraints(edgeConstraints: edgeConstraints, + centerConstraints: centerConstraints, + sizeConstraints: sizeConstraints) + }() + + public var customViewConstraints: SubviewConstraints? + + // MARK: - BaseInitializableView + + open override func addViews() { + super.addViews() + + addSubviews(leadingButton, trailingButton) + } + + open override func configureLayout() { + super.configureLayout() + + for view in [leadingButton, trailingButton] { + view.translatesAutoresizingMaskIntoConstraints = false + } + } + + // MARK: - AppearanceConfigurable + + open func configure(appearance: Appearance) { + configureUIView(appearance: appearance) + + configureContentView(state: appearance.contentViewState) + } + + open func configureContentView(state: ContentViewState) { + var leadingButtonStyle: BaseButtonStyle? + var trailingButtonStyle: BaseButtonStyle? + + switch state { + case let .leadingButton(style): + leadingButtonStyle = style + leadingButtonConstraints.edgeConstraints.trailingConstraint = leftTrailingToSuperviewTrailing + + case let .trailingButton(style): + trailingButtonStyle = style + trailingButtonConstraints.edgeConstraints.leadingConstraint = rightLeadingToSuperviewLeadingConstraint + + case let .buttons(leading, trailing): + leadingButtonStyle = leading + trailingButtonStyle = trailing + + case let .custom(view, appearance): + configureCustomView(view, withLayout: appearance.layout) + view.configureUIView(appearance: appearance) + } + + configure(buttonStyle: leadingButtonStyle, forButton: leadingButton, constraints: leadingButtonConstraints) + configure(buttonStyle: trailingButtonStyle, forButton: trailingButton, constraints: trailingButtonConstraints) + + if let leadingButtonStyle, let trailingButtonStyle { + leadingButtonConstraints.edgeConstraints.trailingConstraint = leftTrailingToRightLeadingConstraint + trailingButtonConstraints.edgeConstraints.leadingConstraint = leftTrailingToRightLeadingConstraint + + let spacing = leadingButtonStyle.appearance.layout.insets.add(\.right, + to: \.left, + of: trailingButtonStyle.appearance.layout.insets) + + leftTrailingToRightLeadingConstraint.setActiveConstantOrDeactivate(constant: spacing) + } + } + + // MARK: - Private methods + + private func configureCustomView(_ view: UIView, withLayout layout: WrappedViewLayout) { + addSubview(view) + + view.translatesAutoresizingMaskIntoConstraints = false + + let edgeConstraints = EdgeConstraints(leadingConstraint: view.leadingAnchor.constraint(equalTo: leadingAnchor), + trailingConstraint: view.trailingAnchor.constraint(equalTo: trailingAnchor), + topConstraint: view.topAnchor.constraint(equalTo: topAnchor), + bottomConstraint: view.bottomAnchor.constraint(equalTo: bottomAnchor)) + + let centerConstraints = CenterConstraints(centerXConstraint: view.centerXAnchor.constraint(equalTo: centerXAnchor), + centerYConstraint: view.centerYAnchor.constraint(equalTo: centerYAnchor)) + + let sizeConstraints = SizeConstraints(widthConstraint: view.widthAnchor.constraint(equalToConstant: .zero), + heightConstraint: view.heightAnchor.constraint(equalToConstant: .zero)) + + let customViewConstraints = SubviewConstraints(edgeConstraints: edgeConstraints, + centerConstraints: centerConstraints, + sizeConstraints: sizeConstraints) + + self.customViewConstraints = customViewConstraints + + customViewConstraints.update(from: layout) + } + + private func configure(buttonStyle: BaseButtonStyle?, + forButton button: StatefulButton, + constraints: SubviewConstraints?) { + + guard let buttonStyle else { + button.isHidden = true + return + } + + button.isHidden = false + + constraints?.update(from: buttonStyle.appearance.layout) + + button.apply(style: buttonStyle) + } +} + +// MARK: - Appearance + +public extension ModalHeaderView { + + final class Appearance: BaseWrappedAppearance, WrappedViewAppearance { + public static var defaultAppearance: Self { + Self(layout: DefaultWrappedLayout(insets: .horizontal(.zero).vertical(top: .zero), + size: .fixedHeight(44))) + } + + public var contentViewState: ContentViewState + + public init(layout: DefaultWrappedLayout = .defaultLayout, + backgroundColor: UIColor = .clear, + border: UIViewBorder = .init(), + shadow: UIViewShadow? = nil, + contentViewState: ContentViewState = .leadingButton(.init())) { + + self.contentViewState = contentViewState + + super.init(layout: layout, backgroundColor: backgroundColor, border: border, shadow: shadow) + } + } +} diff --git a/TIBottomSheet/Sources/BottomSheet/Views/PassthroughDimmedView.swift b/TIBottomSheet/Sources/BottomSheet/Views/PassthroughDimmedView.swift new file mode 100644 index 00000000..64c0e249 --- /dev/null +++ b/TIBottomSheet/Sources/BottomSheet/Views/PassthroughDimmedView.swift @@ -0,0 +1,50 @@ +// +// Copyright (c) 2023 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 PanModal + +open class PassthroughDimmedView: DimmedView { + public weak var hitTestHandlerView: UIView? + + public var isTransparent = false + + public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + guard let hitTestHandlerView else { + return super.hitTest(point, with: event) + } + + return hitTestHandlerView.hitTest(point, with: event) + } + + public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + hitTestHandlerView == nil || super.point(inside: point, with: event) + } + + open override func onChange(dimState: DimmedView.DimState) { + super.onChange(dimState: dimState) + + if isTransparent { + alpha = .zero + } + } +} diff --git a/TIBottomSheet/TIBottomSheet.app/.gitignore b/TIBottomSheet/TIBottomSheet.app/.gitignore new file mode 100644 index 00000000..2784c2d1 --- /dev/null +++ b/TIBottomSheet/TIBottomSheet.app/.gitignore @@ -0,0 +1,3 @@ +**/build/ +**/nef/ +LICENSE diff --git a/TIBottomSheet/TIBottomSheet.app/Contents/Info.plist b/TIBottomSheet/TIBottomSheet.app/Contents/Info.plist new file mode 100644 index 00000000..831ea97a --- /dev/null +++ b/TIBottomSheet/TIBottomSheet.app/Contents/Info.plist @@ -0,0 +1,28 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + launcher + CFBundleIconFile + AppIcon + CFBundleIconName + AppIcon + CFBundleIdentifier + com.fortysevendeg.nef + CFBundleInfoDictionaryVersion + 6.0 + CFBundleSupportedPlatforms + + MacOSX + + LSApplicationCategoryType + public.app-category.developer-tools + LSMinimumSystemVersion + 10.14 + NSHumanReadableCopyright + Copyright © 2019 The nef Authors. All rights reserved. + + diff --git a/TIBottomSheet/TIBottomSheet.app/Contents/MacOS/.gitignore b/TIBottomSheet/TIBottomSheet.app/Contents/MacOS/.gitignore new file mode 100644 index 00000000..18bd1f3b --- /dev/null +++ b/TIBottomSheet/TIBottomSheet.app/Contents/MacOS/.gitignore @@ -0,0 +1,26 @@ +## gitignore nef files +**/build/ +**/nef/ +LICENSE + +## User data +**/xcuserdata/ +podfile.lock +**.DS_Store + +## Obj-C/Swift specific +*.hmap +*.ipa +*.dSYM.zip +*.dSYM + +## CocoaPods +**Pods** + +## Carthage +**Carthage** + +## SPM +.build +.swiftpm +swiftpm diff --git a/TIBottomSheet/TIBottomSheet.app/Contents/MacOS/Podfile b/TIBottomSheet/TIBottomSheet.app/Contents/MacOS/Podfile new file mode 100644 index 00000000..beb2a489 --- /dev/null +++ b/TIBottomSheet/TIBottomSheet.app/Contents/MacOS/Podfile @@ -0,0 +1,13 @@ +ENV["DEVELOPMENT_INSTALL"] = "true" + +source 'https://git.svc.touchin.ru/TouchInstinct/Podspecs.git' + +target 'TIBottomSheet' do + platform :ios, 11 + use_frameworks! + + pod 'TIUIElements', :path => '../../../../TIUIElements/TIUIElements.podspec' + pod 'TIUIKitCore', :path => '../../../../TIUIKitCore/TIUIKitCore.podspec' + pod 'TISwiftUtils', :path => '../../../../TISwiftUtils/TISwiftUtils.podspec' + pod 'TIBottomSheet', :path => '../../../../TIBottomSheet/TIBottomSheet.podspec' +end diff --git a/TIBottomSheet/TIBottomSheet.app/Contents/MacOS/TIBottomSheet.playground/Pages/TIBottomSheet.xcplaygroundpage/Contents.swift b/TIBottomSheet/TIBottomSheet.app/Contents/MacOS/TIBottomSheet.playground/Pages/TIBottomSheet.xcplaygroundpage/Contents.swift new file mode 100644 index 00000000..0813f0d0 --- /dev/null +++ b/TIBottomSheet/TIBottomSheet.app/Contents/MacOS/TIBottomSheet.playground/Pages/TIBottomSheet.xcplaygroundpage/Contents.swift @@ -0,0 +1,108 @@ +/*: + # TIBottomSheet + + TIBottomSheet содержить базовую реализацию модального котроллера и немного видоизмененную библиотеку PanModal. + + ## Базовый контроллер + + Для создания модального котроллера можно унаследоваться от `BaseModalViewController`. Данный клас принимает два generic типа: тип основного контента, тип контента футера. + */ +import TIBottomSheet +import UIKit + +class EmptyViewController: BaseModalViewController { } + +/*: + ## Обертка вокруг существующего контроллера + + Может быть такое, что из уже существующего контроллера нужно сделать модальное окно. С этим может помочь обертка `BaseModalWrapperViewController`. Данный контроллер является наследником BaseModalViewController, что позволяет его настраивать так же, как и базовый модальный котроллер + */ + +import TIUIKitCore + +final class OldMassiveViewController: BaseInitializableViewController { + // some implementation +} + +typealias ModalOldMassiveViewController = BaseModalWrapperViewController + +class PresentingViewController: BaseInitializableViewController { + // some implementation + + @objc private func onButtonTapped() { + presentPanModal(ModalOldMassiveViewController()) + } +} + +/*: + ## Контент модального контроллера + + Модальный котроллер может содержать следующие элементы: `DragView`, `HeaderView`, `FooterView`. Каждый из них является опциональным и без дополнительных настроек не будет показываться. + + DragView - небольшая view, за которую пользователь "держит" модальный контроллер + HeaderView - контейнер, содержащий в себе кнопки назад/закрыть или какие-то другие элементы управления + FooterView - view, располагающаяся внизу контроллера, поверх всего контента (модальный контроллер уже настроен так, чтобы при скролле в самый низ, футер не перекрывал последнюю ячейку) + + Для настройки каждого у котроллера есть свойство `viewControllerAppearance`. Через него будет настраиваться весь контроллер. Однако стоит заметить, что котроллер не будет настраивать передаваимую вью, содержащую основной контент. Стандартно котроллер будет пытаться расположить контент так, чтобы он заполнил все пространство. + + Вот пример настройки внешнего вида так, чтобы был видет dragView и headerView с левой кнопкой: + */ + +let customViewController = BaseModalViewController() +customViewController.viewControllerAppearance = BaseModalViewController.DefaultAppearance.make { + $0.dragViewState = .presented(.defaultAppearance) + $0.headerViewState = .presented(.make { + $0.layout.size = .fixedHeight(52) + $0.backgroundColor = .white + $0.contentViewState = .buttonLeft(.init(titles: [.normal: "Close"], + appearance: .init(stateAppearances: [ + .normal: .init(backgroundColor: .blue) + ]))) + }) +} + +/*: + ## "Якори" контроллера + + Раньше для настройки высоты контроллера необходимо было пользоваться свойствами `longFormHeight`, `shortFormHeight`. В базовом контроллере можно лишь передать список точек на которых контроллер должен будет задержаться: + */ + +let detentsViewController = BaseModalViewController() +detentsViewController.viewControllerAppearance.presentationDetents = [.headerOnly, .height(300), .maxHeight] + +/*: + - headerOnly будет сам пытаться вычеслить высоту хедера и dragView, показывая только их + - height(_) будет показывать контроллер на переданной высоте + - maxHeight - вся высота экрана (до safeArea) + + В данный массив не рекомендуется передавать больше 3 значений, т.к. модальное окно все равно сможет занять только 3 положения на экране. + + ## DimmedView и PassthroughDimmedView + + Для контроля `DimmedView` (затемняющей view) есть отдельное свойство `dimmedView`. Эти классы позволяют настраивать поведение при тапе в затемнённую область и кастомизировать затемнение под ваши нужды. + */ + +let shadowViewController = BaseModalViewController() +let dimmedView = PassthroughDimmedView() +dimmedView.hitTestHandlerView = view +dimmedView.configureUIView(appearance: .init(shadow: UIViewShadow(radius: 8, + color: .black, + opacity: 0.3))) + +shadowViewController.dimmedView = dimmedView + +/*: + ## Контроль закрытия + + `PanModalPresentable` не умеет в настройку закрытия контроллера, делая это самостоятельно через `dismiss(animated:completion:)`. Теперь можно настроить закрытие самостоятельно через свойства: `onTapToDismiss` и `onDragToDismiss`. + + # Взаимодействие с PanModal + + Если нет необходимости или возможности использовать `BaseModalViewController`, вы все так же можете пользоваться протоколом `PanModalRepresentable`. Вот список изменений протокола: + + - Открытие/закрытие модального окна теперь можно настроить с помощью свойств `onTapToDismiss` и `onDragToDismiss` + - Можно настроить промежуточное состояние модального окна с `mediumFormHeight` + - `DimmedView` открыт для наследования и может создаваться в `dimmedView` у + + > Для `BaseModalViewController` все свойства из `PanModalPresentable` все также работают, т.е. вы можете их переопределять, добавлять и изменять по необходимости. + */ diff --git a/TIBottomSheet/TIBottomSheet.app/Contents/MacOS/TIBottomSheet.playground/Sources/NefPlaygroundSupport.swift b/TIBottomSheet/TIBottomSheet.app/Contents/MacOS/TIBottomSheet.playground/Sources/NefPlaygroundSupport.swift new file mode 100644 index 00000000..2ffea80a --- /dev/null +++ b/TIBottomSheet/TIBottomSheet.app/Contents/MacOS/TIBottomSheet.playground/Sources/NefPlaygroundSupport.swift @@ -0,0 +1,30 @@ +import UIKit + +public protocol NefPlaygroundLiveViewable {} +extension UIView: NefPlaygroundLiveViewable {} +extension UIViewController: NefPlaygroundLiveViewable {} + +#if NOT_IN_PLAYGROUND +public enum Nef { + public enum Playground { + public static func liveView(_ view: NefPlaygroundLiveViewable) {} + public static func needsIndefiniteExecution(_ state: Bool) {} + } +} + +#else +import PlaygroundSupport + +public enum Nef { + public enum Playground { + public static func liveView(_ view: NefPlaygroundLiveViewable) { + PlaygroundPage.current.liveView = (view as! PlaygroundLiveViewable) + } + + public static func needsIndefiniteExecution(_ state: Bool) { + PlaygroundPage.current.needsIndefiniteExecution = state + } + } +} + +#endif diff --git a/TIBottomSheet/TIBottomSheet.app/Contents/MacOS/TIBottomSheet.playground/Sources/NefTest.swift b/TIBottomSheet/TIBottomSheet.app/Contents/MacOS/TIBottomSheet.playground/Sources/NefTest.swift new file mode 100644 index 00000000..fe15078e --- /dev/null +++ b/TIBottomSheet/TIBottomSheet.app/Contents/MacOS/TIBottomSheet.playground/Sources/NefTest.swift @@ -0,0 +1,47 @@ +import Foundation +import XCTest + +public extension Nef { + + static func run(testCase class: T.Type) { + startTestObserver() + T.defaultTestSuite.run() + } + + static private func startTestObserver() { + _ = testObserverInstalled + } + + static private var testObserverInstalled = { () -> NefTestFailObserver in + let testObserver = NefTestFailObserver() + XCTestObservationCenter.shared.addTestObserver(testObserver) + return testObserver + }() +} + +// MARK: enrich the output for XCTest +fileprivate class NefTestFailObserver: NSObject, XCTestObservation { + + private var numberOfFailedTests = 0 + + func testSuiteWillStart(_ testSuite: XCTestSuite) { + numberOfFailedTests = 0 + } + + func testSuiteDidFinish(_ testSuite: XCTestSuite) { + if numberOfFailedTests > 0 { + print("💢 Test Suite '\(testSuite.name)' finished with \(numberOfFailedTests) failed \(numberOfFailedTests > 1 ? "tests" : "test").") + } else { + print("🔅 Test Suite '\(testSuite.name)' finished successfully.") + } + } + + func testCase(_ testCase: XCTestCase, + didFailWithDescription description: String, + inFile filePath: String?, + atLine lineNumber: Int) { + + numberOfFailedTests += 1 + print("❗️Test Fail '\(testCase.name)':\(UInt(lineNumber)): \(description.description)") + } +} diff --git a/TIBottomSheet/TIBottomSheet.app/Contents/MacOS/TIBottomSheet.playground/contents.xcplayground b/TIBottomSheet/TIBottomSheet.app/Contents/MacOS/TIBottomSheet.playground/contents.xcplayground new file mode 100644 index 00000000..00daa653 --- /dev/null +++ b/TIBottomSheet/TIBottomSheet.app/Contents/MacOS/TIBottomSheet.playground/contents.xcplayground @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/TIBottomSheet/TIBottomSheet.app/Contents/MacOS/TIBottomSheet.xcodeproj/project.pbxproj b/TIBottomSheet/TIBottomSheet.app/Contents/MacOS/TIBottomSheet.xcodeproj/project.pbxproj new file mode 100644 index 00000000..c7711f67 --- /dev/null +++ b/TIBottomSheet/TIBottomSheet.app/Contents/MacOS/TIBottomSheet.xcodeproj/project.pbxproj @@ -0,0 +1,396 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 83C08983988F66570478C40D /* Pods_TIBottomSheet.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8EF3488B86B483233C2CC631 /* Pods_TIBottomSheet.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 7B6955D74676A5427AC42234 /* Pods-TIBottomSheet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TIBottomSheet.debug.xcconfig"; path = "Target Support Files/Pods-TIBottomSheet/Pods-TIBottomSheet.debug.xcconfig"; sourceTree = ""; }; + 8BACBE8322576CAD00266845 /* TIBottomSheet.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TIBottomSheet.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 8BACBE8622576CAD00266845 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 8EF3488B86B483233C2CC631 /* Pods_TIBottomSheet.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_TIBottomSheet.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + AA57D8210790AD14BCC54A7E /* Pods-TIBottomSheet.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TIBottomSheet.release.xcconfig"; path = "Target Support Files/Pods-TIBottomSheet/Pods-TIBottomSheet.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 8BACBE8022576CAD00266845 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 83C08983988F66570478C40D /* Pods_TIBottomSheet.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 11F06D2789C6CF40767861CF /* Frameworks */ = { + isa = PBXGroup; + children = ( + 8EF3488B86B483233C2CC631 /* Pods_TIBottomSheet.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 1F7782E3A7AD7291B7C09F56 /* Pods */ = { + isa = PBXGroup; + children = ( + 7B6955D74676A5427AC42234 /* Pods-TIBottomSheet.debug.xcconfig */, + AA57D8210790AD14BCC54A7E /* Pods-TIBottomSheet.release.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 8B39A26221D40F8700DE2643 = { + isa = PBXGroup; + children = ( + 8BACBE8422576CAD00266845 /* TIBottomSheet */, + 8B39A26C21D40F8700DE2643 /* Products */, + 1F7782E3A7AD7291B7C09F56 /* Pods */, + 11F06D2789C6CF40767861CF /* Frameworks */, + ); + sourceTree = ""; + }; + 8B39A26C21D40F8700DE2643 /* Products */ = { + isa = PBXGroup; + children = ( + 8BACBE8322576CAD00266845 /* TIBottomSheet.framework */, + ); + name = Products; + sourceTree = ""; + }; + 8BACBE8422576CAD00266845 /* TIBottomSheet */ = { + isa = PBXGroup; + children = ( + 8BACBE8622576CAD00266845 /* Info.plist */, + ); + path = TIBottomSheet; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 8BACBE7E22576CAD00266845 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 8BACBE8222576CAD00266845 /* TIBottomSheet */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8BACBE8A22576CAD00266845 /* Build configuration list for PBXNativeTarget "TIBottomSheet" */; + buildPhases = ( + 4E98D4C60DCD00EB801E579E /* [CP] Check Pods Manifest.lock */, + 8BACBE7E22576CAD00266845 /* Headers */, + 8BACBE7F22576CAD00266845 /* Sources */, + 8BACBE8022576CAD00266845 /* Frameworks */, + 8BACBE8122576CAD00266845 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = TIBottomSheet; + productName = TIBottomSheet2; + productReference = 8BACBE8322576CAD00266845 /* TIBottomSheet.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 8B39A26321D40F8700DE2643 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1010; + LastUpgradeCheck = 1200; + ORGANIZATIONNAME = "47 Degrees"; + TargetAttributes = { + 8BACBE8222576CAD00266845 = { + CreatedOnToolsVersion = 10.1; + }; + }; + }; + buildConfigurationList = 8B39A26621D40F8700DE2643 /* Build configuration list for PBXProject "TIBottomSheet" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 8B39A26221D40F8700DE2643; + productRefGroup = 8B39A26C21D40F8700DE2643 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 8BACBE8222576CAD00266845 /* TIBottomSheet */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 8BACBE8122576CAD00266845 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 4E98D4C60DCD00EB801E579E /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-TIBottomSheet-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 8BACBE7F22576CAD00266845 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 8B39A27721D40F8800DE2643 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_TESTING_SEARCH_PATHS = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 8B39A27821D40F8800DE2643 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTING_SEARCH_PATHS = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 8BACBE8822576CAD00266845 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7B6955D74676A5427AC42234 /* Pods-TIBottomSheet.debug.xcconfig */; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Manual; + CURRENT_TIBottomSheet_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "$(SRCROOT)/TIBottomSheet/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 12.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.47deg.ios.TIBottomSheet; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 8BACBE8922576CAD00266845 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AA57D8210790AD14BCC54A7E /* Pods-TIBottomSheet.release.xcconfig */; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Manual; + CURRENT_TIBottomSheet_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "$(SRCROOT)/TIBottomSheet/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 12.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.47deg.ios.TIBottomSheet; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 8B39A26621D40F8700DE2643 /* Build configuration list for PBXProject "TIBottomSheet" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8B39A27721D40F8800DE2643 /* Debug */, + 8B39A27821D40F8800DE2643 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 8BACBE8A22576CAD00266845 /* Build configuration list for PBXNativeTarget "TIBottomSheet" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8BACBE8822576CAD00266845 /* Debug */, + 8BACBE8922576CAD00266845 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 8B39A26321D40F8700DE2643 /* Project object */; +} diff --git a/TIBottomSheet/TIBottomSheet.app/Contents/MacOS/TIBottomSheet.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/TIBottomSheet/TIBottomSheet.app/Contents/MacOS/TIBottomSheet.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..bb29c43f --- /dev/null +++ b/TIBottomSheet/TIBottomSheet.app/Contents/MacOS/TIBottomSheet.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/TIBottomSheet/TIBottomSheet.app/Contents/MacOS/TIBottomSheet.xcodeproj/xcshareddata/xcschemes/TIBottomSheet.xcscheme b/TIBottomSheet/TIBottomSheet.app/Contents/MacOS/TIBottomSheet.xcodeproj/xcshareddata/xcschemes/TIBottomSheet.xcscheme new file mode 100644 index 00000000..575e67ee --- /dev/null +++ b/TIBottomSheet/TIBottomSheet.app/Contents/MacOS/TIBottomSheet.xcodeproj/xcshareddata/xcschemes/TIBottomSheet.xcscheme @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/TIBottomSheet/TIBottomSheet.app/Contents/MacOS/TIBottomSheet.xcworkspace/contents.xcworkspacedata b/TIBottomSheet/TIBottomSheet.app/Contents/MacOS/TIBottomSheet.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..6389ec4c --- /dev/null +++ b/TIBottomSheet/TIBottomSheet.app/Contents/MacOS/TIBottomSheet.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/TIBottomSheet/TIBottomSheet.app/Contents/MacOS/TIBottomSheet/Info.plist b/TIBottomSheet/TIBottomSheet.app/Contents/MacOS/TIBottomSheet/Info.plist new file mode 100644 index 00000000..98d14f60 --- /dev/null +++ b/TIBottomSheet/TIBottomSheet.app/Contents/MacOS/TIBottomSheet/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1.0 + NSHumanReadableCopyright + Copyright © 2019. The nef authors. + + diff --git a/TIBottomSheet/TIBottomSheet.app/Contents/MacOS/launcher b/TIBottomSheet/TIBottomSheet.app/Contents/MacOS/launcher new file mode 100755 index 00000000..7451a86d --- /dev/null +++ b/TIBottomSheet/TIBottomSheet.app/Contents/MacOS/launcher @@ -0,0 +1,6 @@ +#!/bin/bash + +workspace="TIBottomSheet.xcworkspace" +workspacePath=$(echo "$0" | rev | cut -f2- -d '/' | rev) + +open "`pwd`/$workspacePath/$workspace" diff --git a/TIBottomSheet/TIBottomSheet.app/Contents/Resources/AppIcon.icns b/TIBottomSheet/TIBottomSheet.app/Contents/Resources/AppIcon.icns new file mode 100644 index 00000000..32814f1c Binary files /dev/null and b/TIBottomSheet/TIBottomSheet.app/Contents/Resources/AppIcon.icns differ diff --git a/TIBottomSheet/TIBottomSheet.app/Contents/Resources/Assets.car b/TIBottomSheet/TIBottomSheet.app/Contents/Resources/Assets.car new file mode 100644 index 00000000..79d9ea89 Binary files /dev/null and b/TIBottomSheet/TIBottomSheet.app/Contents/Resources/Assets.car differ diff --git a/TIBottomSheet/TIBottomSheet.playground b/TIBottomSheet/TIBottomSheet.playground new file mode 120000 index 00000000..126d18f1 --- /dev/null +++ b/TIBottomSheet/TIBottomSheet.playground @@ -0,0 +1 @@ +TIBottomSheet.app/Contents/MacOS/TIBottomSheet.playground \ No newline at end of file diff --git a/TIBottomSheet/TIBottomSheet.podspec b/TIBottomSheet/TIBottomSheet.podspec new file mode 100644 index 00000000..bfaeb5a1 --- /dev/null +++ b/TIBottomSheet/TIBottomSheet.podspec @@ -0,0 +1,29 @@ +Pod::Spec.new do |s| + s.name = 'TIBottomSheet' + s.version = '1.51.0' + s.summary = 'Base models for creating bottom sheet view controllers' + s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name + s.license = { :type => 'MIT', :file => 'LICENSE' } + s.author = { 'castlele' => 'nikita.semenov@touchin.ru', + 'petropavel13' => 'ivan.smolin@touchin.ru'} + s.source = { :git => 'https://git.svc.touchin.ru/TouchInstinct/LeadKit.git', :tag => s.version.to_s } + + s.ios.deployment_target = '11.0' + s.swift_versions = ['5.7'] + + sources = 'Sources/**/*' + if ENV["DEVELOPMENT_INSTALL"] # installing using :path => + s.source_files = sources + s.exclude_files = s.name + '.app' + else + s.source_files = s.name + '/' + sources + s.exclude_files = s.name + '/*.app' + end + + s.dependency 'TIUIElements', s.version.to_s + s.dependency 'TIUIKitCore', s.version.to_s + s.dependency 'TISwiftUtils', s.version.to_s + s.dependency 'PanModal', '~> 1.3.0' + +end + diff --git a/TICoreGraphicsUtils/TICoreGraphicsUtils.podspec b/TICoreGraphicsUtils/TICoreGraphicsUtils.podspec index b2754d8d..e36a6951 100644 --- a/TICoreGraphicsUtils/TICoreGraphicsUtils.podspec +++ b/TICoreGraphicsUtils/TICoreGraphicsUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TICoreGraphicsUtils' - s.version = '1.50.0' + s.version = '1.51.0' s.summary = 'CoreGraphics drawing helpers' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIDeeplink/TIDeeplink.podspec b/TIDeeplink/TIDeeplink.podspec index 49c419a0..f018e444 100644 --- a/TIDeeplink/TIDeeplink.podspec +++ b/TIDeeplink/TIDeeplink.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIDeeplink' - s.version = '1.50.0' + s.version = '1.51.0' s.summary = 'Deeplink service API' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIDeveloperUtils/TIDeveloperUtils.podspec b/TIDeveloperUtils/TIDeveloperUtils.podspec index 62bf96fc..00cb5431 100644 --- a/TIDeveloperUtils/TIDeveloperUtils.podspec +++ b/TIDeveloperUtils/TIDeveloperUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIDeveloperUtils' - s.version = '1.50.0' + s.version = '1.51.0' s.summary = 'Universal web view API' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIEcommerce/Sources/Filters/FiltersViews/ListFilters/FiltersTableViewCell/DefaultFilterTableViewCell.swift b/TIEcommerce/Sources/Filters/FiltersViews/ListFilters/FiltersTableViewCell/DefaultFilterTableViewCell.swift index 759c9270..cd38ae79 100644 --- a/TIEcommerce/Sources/Filters/FiltersViews/ListFilters/FiltersTableViewCell/DefaultFilterTableViewCell.swift +++ b/TIEcommerce/Sources/Filters/FiltersViews/ListFilters/FiltersTableViewCell/DefaultFilterTableViewCell.swift @@ -60,7 +60,7 @@ open class DefaultFilterTableViewCell: ContainerTableViewCell // MARK: - Open methods open func updateAppearance(with appearance: FilterCellStateAppearance) { - contentInsets = appearance.contentInsets + wrappedContentInsets = appearance.contentInsets wrappedView.textColor = appearance.fontColor wrappedView.images = appearance.stateImages ?? [:] diff --git a/TIEcommerce/Sources/Filters/FiltersViews/TagsFilters/FiltersCollectionCell/DefaultFilterCollectionCell.swift b/TIEcommerce/Sources/Filters/FiltersViews/TagsFilters/FiltersCollectionCell/DefaultFilterCollectionCell.swift index 19895540..605d9217 100644 --- a/TIEcommerce/Sources/Filters/FiltersViews/TagsFilters/FiltersCollectionCell/DefaultFilterCollectionCell.swift +++ b/TIEcommerce/Sources/Filters/FiltersViews/TagsFilters/FiltersCollectionCell/DefaultFilterCollectionCell.swift @@ -56,7 +56,7 @@ open class DefaultFilterCollectionCell: ContainerCollectionViewCell, Co // MARK: - Open methdos open func updateAppearance(with appearance: FilterCellStateAppearance) { - contentInsets = appearance.contentInsets + wrappedContentInsets = appearance.contentInsets wrappedView.textColor = appearance.fontColor backgroundColor = appearance.backgroundColor diff --git a/TIEcommerce/TIEcommerce.podspec b/TIEcommerce/TIEcommerce.podspec index 46af1443..c3b053a7 100644 --- a/TIEcommerce/TIEcommerce.podspec +++ b/TIEcommerce/TIEcommerce.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIEcommerce' - s.version = '1.50.0' + s.version = '1.51.0' s.summary = 'Cart, products, promocodes, bonuses and other related actions' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIFoundationUtils/TIFoundationUtils.podspec b/TIFoundationUtils/TIFoundationUtils.podspec index 1ae0655b..98183c7d 100644 --- a/TIFoundationUtils/TIFoundationUtils.podspec +++ b/TIFoundationUtils/TIFoundationUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIFoundationUtils' - s.version = '1.50.0' + s.version = '1.51.0' s.summary = 'Set of helpers for Foundation framework classes.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIGoogleMapUtils/TIGoogleMapUtils.podspec b/TIGoogleMapUtils/TIGoogleMapUtils.podspec index 7f8ee7dc..f799cd0f 100644 --- a/TIGoogleMapUtils/TIGoogleMapUtils.podspec +++ b/TIGoogleMapUtils/TIGoogleMapUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIGoogleMapUtils' - s.version = '1.50.0' + s.version = '1.51.0' s.summary = 'Set of helpers for map objects clustering and interacting using Google Maps SDK.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIKeychainUtils/TIKeychainUtils.podspec b/TIKeychainUtils/TIKeychainUtils.podspec index f0a30835..d22109cf 100644 --- a/TIKeychainUtils/TIKeychainUtils.podspec +++ b/TIKeychainUtils/TIKeychainUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIKeychainUtils' - s.version = '1.50.0' + s.version = '1.51.0' s.summary = 'Set of helpers for Keychain classes.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TILogging/TILogging.podspec b/TILogging/TILogging.podspec index e12e9b9e..03ac88c1 100644 --- a/TILogging/TILogging.podspec +++ b/TILogging/TILogging.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TILogging' - s.version = '1.50.0' + s.version = '1.51.0' s.summary = 'Logging for TI libraries.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIMapUtils/TIMapUtils.podspec b/TIMapUtils/TIMapUtils.podspec index d7ed6cb3..6c0a706a 100644 --- a/TIMapUtils/TIMapUtils.podspec +++ b/TIMapUtils/TIMapUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIMapUtils' - s.version = '1.50.0' + s.version = '1.51.0' s.summary = 'Set of helpers for map objects clustering and interacting.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIMoyaNetworking/TIMoyaNetworking.podspec b/TIMoyaNetworking/TIMoyaNetworking.podspec index 2af06524..0ef8a3b3 100644 --- a/TIMoyaNetworking/TIMoyaNetworking.podspec +++ b/TIMoyaNetworking/TIMoyaNetworking.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIMoyaNetworking' - s.version = '1.50.0' + s.version = '1.51.0' s.summary = 'Moya + Swagger network service.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TINetworking/TINetworking.podspec b/TINetworking/TINetworking.podspec index bd4ecc20..5e0bba93 100644 --- a/TINetworking/TINetworking.podspec +++ b/TINetworking/TINetworking.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TINetworking' - s.version = '1.50.0' + s.version = '1.51.0' s.summary = 'Swagger-frendly networking layer helpers.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TINetworkingCache/TINetworkingCache.podspec b/TINetworkingCache/TINetworkingCache.podspec index bfadd0f1..0e947440 100644 --- a/TINetworkingCache/TINetworkingCache.podspec +++ b/TINetworkingCache/TINetworkingCache.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TINetworkingCache' - s.version = '1.50.0' + s.version = '1.51.0' s.summary = 'Caching results of EndpointRequests.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIPagination/TIPagination.podspec b/TIPagination/TIPagination.podspec index a99e7deb..af15b71b 100644 --- a/TIPagination/TIPagination.podspec +++ b/TIPagination/TIPagination.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIPagination' - s.version = '1.50.0' + s.version = '1.51.0' s.summary = 'Generic pagination component.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TISwiftUICore/TISwiftUICore.podspec b/TISwiftUICore/TISwiftUICore.podspec index b1f66557..e8f73430 100644 --- a/TISwiftUICore/TISwiftUICore.podspec +++ b/TISwiftUICore/TISwiftUICore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TISwiftUICore' - s.version = '1.50.0' + s.version = '1.51.0' s.summary = 'Core UI elements: protocols, views and helpers.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TISwiftUtils/Sources/Extensions/Array/Array+Uniqued.swift b/TISwiftUtils/Sources/Extensions/Array/Array+Uniqued.swift new file mode 100644 index 00000000..a22227ff --- /dev/null +++ b/TISwiftUtils/Sources/Extensions/Array/Array+Uniqued.swift @@ -0,0 +1,27 @@ +// +// Copyright (c) 2023 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. +// + +extension Array where Element: Hashable { + public func uniqued() -> [Element] { + Array(Set(self)) + } +} diff --git a/TISwiftUtils/TISwiftUtils.podspec b/TISwiftUtils/TISwiftUtils.podspec index 61a5f078..1451f5f8 100644 --- a/TISwiftUtils/TISwiftUtils.podspec +++ b/TISwiftUtils/TISwiftUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TISwiftUtils' - s.version = '1.50.0' + s.version = '1.51.0' s.summary = 'Bunch of useful helpers for Swift development.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TITableKitUtils/TITableKitUtils.podspec b/TITableKitUtils/TITableKitUtils.podspec index 15c646b7..a31eaac8 100644 --- a/TITableKitUtils/TITableKitUtils.podspec +++ b/TITableKitUtils/TITableKitUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TITableKitUtils' - s.version = '1.50.0' + s.version = '1.51.0' s.summary = 'Set of helpers for TableKit classes.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TITextProcessing/TITextProcessing.podspec b/TITextProcessing/TITextProcessing.podspec index 404f29ff..8813a18e 100644 --- a/TITextProcessing/TITextProcessing.podspec +++ b/TITextProcessing/TITextProcessing.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TITextProcessing' - s.version = '1.50.0' + s.version = '1.51.0' s.summary = 'A text processing service helping to get a text mask and a placeholder from incoming regex.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIUIElements/Sources/Appearance/UIButton+AppearanceConfigurable.swift b/TIUIElements/Sources/Appearance/UIButton+AppearanceConfigurable.swift index 6c7b56a5..00afb6a9 100644 --- a/TIUIElements/Sources/Appearance/UIButton+AppearanceConfigurable.swift +++ b/TIUIElements/Sources/Appearance/UIButton+AppearanceConfigurable.swift @@ -30,21 +30,21 @@ extension UIButton { appearance.textAttributes? .configure(button: self, with: title(for: state), - for: state) + for: state) if #available(iOS 15, *) { var config = configuration ?? .plain() configureInsets(in: &config, - contentInsets: appearance.contentLayout.contentInsets, - titleInsets: appearance.contentLayout.titleInsets, - imageInsets: appearance.contentLayout.imageInsets) + contentInsets: appearance.contentLayout.contentInsets.replacingNan(with: .zero), + titleInsets: appearance.contentLayout.titleInsets.replacingNan(with: .zero), + imageInsets: appearance.contentLayout.imageInsets.replacingNan(with: .zero)) configuration = config } else { - contentEdgeInsets = appearance.contentLayout.contentInsets - titleEdgeInsets = appearance.contentLayout.titleInsets - imageEdgeInsets = appearance.contentLayout.imageInsets + contentEdgeInsets = appearance.contentLayout.contentInsets.replacingNan(with: .zero) + titleEdgeInsets = appearance.contentLayout.titleInsets.replacingNan(with: .zero) + imageEdgeInsets = appearance.contentLayout.imageInsets.replacingNan(with: .zero) } super.configureUIView(appearance: appearance) @@ -67,7 +67,7 @@ extension UIButton { currentImage ] .compactMap { $0 } - .first { !($0.size.width.isZero || $0.size.height.isZero) } != nil + .contains { !($0.size.width.isZero || $0.size.height.isZero) } if hasNonEmptyImage { let leadingContentInset: CGFloat diff --git a/TIUIElements/Sources/Appearance/UIView+ViewLayout.swift b/TIUIElements/Sources/Appearance/UIView+ViewLayout.swift index 94904d82..4a7118a3 100644 --- a/TIUIElements/Sources/Appearance/UIView+ViewLayout.swift +++ b/TIUIElements/Sources/Appearance/UIView+ViewLayout.swift @@ -53,7 +53,7 @@ extension UIView { public var centerOffset: UIOffset public var insets: UIEdgeInsets - public init(insets: UIEdgeInsets = .zero, + public init(insets: UIEdgeInsets = .nan, size: CGSize = .infinity, centerOffset: UIOffset = .nan) { @@ -73,7 +73,7 @@ extension UIView { open class BaseSpecedWrappedLayout: BaseWrappedLayout { public var spacing: CGFloat - public init(insets: UIEdgeInsets = .zero, + public init(insets: UIEdgeInsets = .nan, size: CGSize = .infinity, centerOffset: UIOffset = .nan, spacing: CGFloat = .zero) { @@ -98,7 +98,7 @@ extension UIView { public var distribution: UIStackView.Distribution public var alignment: UIStackView.Alignment - public init(insets: UIEdgeInsets = .zero, + public init(insets: UIEdgeInsets = .nan, size: CGSize = .infinity, centerOffset: UIOffset = .nan, spacing: CGFloat = .zero, diff --git a/TIUIElements/Sources/Separators/ContainerSeparatorTableViewCell.swift b/TIUIElements/Sources/Separators/ContainerSeparatorTableViewCell.swift index 1f34f887..17a54326 100644 --- a/TIUIElements/Sources/Separators/ContainerSeparatorTableViewCell.swift +++ b/TIUIElements/Sources/Separators/ContainerSeparatorTableViewCell.swift @@ -27,15 +27,13 @@ open class ContainerSeparatorTableViewCell: ContainerTableViewCell private lazy var topSeparatorView = createTopSeparator() private lazy var bottomSeparatorView = createBottomSeparator() - private var topViewLeftConstraint: NSLayoutConstraint? - private var topViewRightConstraint: NSLayoutConstraint? - private var topViewTopConstraint: NSLayoutConstraint? - private var topViewHeightConstraint: NSLayoutConstraint? + private lazy var topSeparatorConstraints: SubviewConstraints = { + subviewConstraints(for: topSeparatorView) + }() - private var bottomViewLeftConstraint: NSLayoutConstraint? - private var bottomViewRightConstraint: NSLayoutConstraint? - private var bottomViewBottomConstraint: NSLayoutConstraint? - private var bottomViewHeightConstraint: NSLayoutConstraint? + private lazy var bottomSeparatorConstraints: SubviewConstraints = { + subviewConstraints(for: bottomSeparatorView) + }() open func createTopSeparator() -> UIView { .init() @@ -93,41 +91,17 @@ open class ContainerSeparatorTableViewCell: ContainerTableViewCell open override func configureLayout() { super.configureLayout() - if let separatorSuperview = topSeparatorView.superview { - topViewTopConstraint = topSeparatorView.topAnchor.constraint(equalTo: separatorSuperview.topAnchor) - topViewRightConstraint = separatorSuperview.rightAnchor.constraint(equalTo: topSeparatorView.rightAnchor) - topViewLeftConstraint = topSeparatorView.leftAnchor.constraint(equalTo: separatorSuperview.leftAnchor) + for view in [topSeparatorView, bottomSeparatorView] { + view.translatesAutoresizingMaskIntoConstraints = false } - - topViewHeightConstraint = topSeparatorView.heightAnchor.constraint(equalToConstant: 1) - - if let separatorSuperview = topSeparatorView.superview { - bottomViewRightConstraint = separatorSuperview.rightAnchor.constraint(equalTo: bottomSeparatorView.rightAnchor) - bottomViewLeftConstraint = bottomSeparatorView.leftAnchor.constraint(equalTo: separatorSuperview.leftAnchor) - bottomViewBottomConstraint = bottomSeparatorView.bottomAnchor.constraint(equalTo: separatorSuperview.bottomAnchor) - } - - bottomViewHeightConstraint = bottomSeparatorView.heightAnchor.constraint(equalToConstant: 1) - - NSLayoutConstraint.activate([ - topViewTopConstraint, - topViewRightConstraint, - topViewLeftConstraint, - topViewHeightConstraint, - bottomViewRightConstraint, - bottomViewLeftConstraint, - bottomViewBottomConstraint, - bottomViewHeightConstraint - ].compactMap { $0 }) } open override func configureAppearance() { super.configureAppearance() - [topSeparatorView, bottomSeparatorView].forEach { - $0.isHidden = true - $0.backgroundColor = .black - $0.translatesAutoresizingMaskIntoConstraints = false + for view in [topSeparatorView, bottomSeparatorView] { + view.isHidden = true + view.backgroundColor = .black } } @@ -136,20 +110,37 @@ open class ContainerSeparatorTableViewCell: ContainerTableViewCell private func updateTopSeparator(with appearance: SeparatorAppearance) { topSeparatorView.configureUIView(appearance: appearance) - topViewHeightConstraint?.constant = appearance.layout.size.height - - topViewTopConstraint?.constant = appearance.layout.insets.top - topViewLeftConstraint?.constant = appearance.layout.insets.left - topViewRightConstraint?.constant = appearance.layout.insets.right + topSeparatorConstraints.update(from: appearance.layout) } private func updateBottomSeparator(with appearance: SeparatorAppearance) { bottomSeparatorView.configureUIView(appearance: appearance) - bottomViewHeightConstraint?.constant = appearance.layout.size.height + bottomSeparatorConstraints.update(from: appearance.layout) + } - bottomViewBottomConstraint?.constant = appearance.layout.insets.bottom - bottomViewLeftConstraint?.constant = appearance.layout.insets.left - bottomViewRightConstraint?.constant = appearance.layout.insets.right + private func subviewConstraints(for seperatorView: UIView) -> SubviewConstraints { + let leadingConstraint = seperatorView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor) + let trailingConstraint = seperatorView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor) + let topConstraint = seperatorView.topAnchor.constraint(equalTo: contentView.topAnchor) + let bottomConstraint = seperatorView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) + + let edgeConstraints = EdgeConstraints(leadingConstraint: leadingConstraint, + trailingConstraint: trailingConstraint, + topConstraint: topConstraint, + bottomConstraint: bottomConstraint) + + let centerXConstraint = seperatorView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor) + let centerYConstraint = seperatorView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor) + + let centerConstraints = CenterConstraints(centerXConstraint: centerXConstraint, + centerYConstraint: centerYConstraint) + + let sizeConstraints = SizeConstraints(widthConstraint: seperatorView.widthAnchor.constraint(equalToConstant: .zero), + heightConstraint: seperatorView.heightAnchor.constraint(equalToConstant: .zero)) + + return SubviewConstraints(edgeConstraints: edgeConstraints, + centerConstraints: centerConstraints, + sizeConstraints: sizeConstraints) } } diff --git a/TIUIElements/Sources/Views/BaseInitializeableViews/BaseInitializeableScrollView.swift b/TIUIElements/Sources/Views/BaseInitializeableViews/BaseInitializeableScrollView.swift new file mode 100644 index 00000000..084e48fc --- /dev/null +++ b/TIUIElements/Sources/Views/BaseInitializeableViews/BaseInitializeableScrollView.swift @@ -0,0 +1,70 @@ +// +// Copyright (c) 2023 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 UIKit + +open class BaseInitializeableScrollView: UIScrollView, InitializableViewProtocol { + public var callbacks: [ViewCallbacks] = [] + + override public init(frame: CGRect) { + super.init(frame: frame) + + initializeView() + } + + required public init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + + initializeView() + } + + override open func layoutSubviews() { + super.layoutSubviews() + + for callback in callbacks { + callback.onDidLayoutSubviews() + } + } + + // 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/TIUIElements/Sources/Views/ListItemView/BaseListItemView.swift b/TIUIElements/Sources/Views/ListItemView/BaseListItemView.swift index e5a9aa5e..f1c9bbf7 100644 --- a/TIUIElements/Sources/Views/ListItemView/BaseListItemView.swift +++ b/TIUIElements/Sources/Views/ListItemView/BaseListItemView.swift @@ -38,29 +38,47 @@ open class BaseListItemView { public var titleSubtitle: DefaultTitleSubtitleViewModel public var controlsViewAxis: NSLayoutConstraint.Axis public var appearance: Appearance - public var buttonsStyles: [PlaceholderButtonStyle] + public var buttonsStyles: [BaseButtonStyle] public init(titleSubtitle: DefaultTitleSubtitleViewModel = .init(), appearance: Appearance = .defaultAppearance, controlsViewAxis: NSLayoutConstraint.Axis = .vertical, - buttonsStyles: [PlaceholderButtonStyle] = []) { + buttonsStyles: [BaseButtonStyle] = []) { self.titleSubtitle = titleSubtitle self.appearance = appearance diff --git a/TIUIElements/Sources/Views/Placeholder/Styles/DefaultPlaceholderStyle.swift b/TIUIElements/Sources/Views/Placeholder/Styles/DefaultPlaceholderStyle.swift index 6e1f9cb0..1aa7b2b5 100644 --- a/TIUIElements/Sources/Views/Placeholder/Styles/DefaultPlaceholderStyle.swift +++ b/TIUIElements/Sources/Views/Placeholder/Styles/DefaultPlaceholderStyle.swift @@ -36,7 +36,7 @@ public final class DefaultPlaceholderStyle: BasePlaceholderStyle) -> Self { - let buttonStyle = PlaceholderButtonStyle() + func withButton(_ builder: ParameterClosure) -> Self { + let buttonStyle = BaseButtonStyle() builder(buttonStyle) buttonsStyles.append(buttonStyle) @@ -74,7 +74,7 @@ public extension PlaceholderStyle { func withButtons(_ amount: Int, axis: NSLayoutConstraint.Axis, - _ builder: (Int, PlaceholderButtonStyle) -> Void) -> Self { + _ builder: (Int, BaseButtonStyle) -> Void) -> Self { controlsViewAxis = axis @@ -83,7 +83,7 @@ public extension PlaceholderStyle { builder(index, buttonStyle) } else { - let buttonStyle = PlaceholderButtonStyle() + let buttonStyle = BaseButtonStyle() builder(index, buttonStyle) buttonsStyles.append(buttonStyle) diff --git a/TIUIElements/Sources/Views/Placeholder/Views/BasePlaceholderImageView.swift b/TIUIElements/Sources/Views/Placeholder/Views/BasePlaceholderImageView.swift index 0a4f8ee7..a0d174b3 100644 --- a/TIUIElements/Sources/Views/Placeholder/Views/BasePlaceholderImageView.swift +++ b/TIUIElements/Sources/Views/Placeholder/Views/BasePlaceholderImageView.swift @@ -33,19 +33,19 @@ open class BasePlaceholderImageView: UIImageView, public private(set) lazy var wrappedView = Placeholder() - public var contentInsets: UIEdgeInsets = .zero { + public var wrappedContentInsets: UIEdgeInsets = .zero { didSet { update(subviewConstraints: placeholderConstraints) } } - public var contentSize: CGSize = .infinity { + public var wrappedContentSize: CGSize = .infinity { didSet { update(subviewConstraints: placeholderConstraints) } } - public var contentCenterOffset: UIOffset = .nan { + public var wrappedContentCenterOffset: UIOffset = .nan { didSet { update(subviewConstraints: placeholderConstraints) } @@ -118,7 +118,9 @@ open class BasePlaceholderImageView: UIImageView, // MARK: - Open methods - open func configureBasePlaceholder(appearance: BaseWrappedViewHolderAppearance) { + open func configureBasePlaceholder(appearance: BaseWrappedViewHolderAppearance< + some BaseWrappedAppearance, + some WrappedViewLayout>) { configureUIView(appearance: appearance) wrappedView.configureUIView(appearance: appearance.subviewAppearance) diff --git a/TIUIElements/Sources/Views/Placeholder/Views/BasePlaceholderView.swift b/TIUIElements/Sources/Views/Placeholder/Views/BasePlaceholderView.swift index 982a9dbc..772d4538 100644 --- a/TIUIElements/Sources/Views/Placeholder/Views/BasePlaceholderView.swift +++ b/TIUIElements/Sources/Views/Placeholder/Views/BasePlaceholderView.swift @@ -31,48 +31,78 @@ open class BasePlaceholderView: BaseInitializableView { public let controlsStackView = UIStackView() public private(set) lazy var imageViewConstraints: SubviewConstraints = { - SubviewConstraints(edgeConstraints: EdgeConstraints(leadingConstraint: imageView.leadingAnchor.constraint(equalTo: leadingAnchor), - trailingConstraint: imageView.trailingAnchor.constraint(equalTo: trailingAnchor), - topConstraint: imageView.topAnchor.constraint(equalTo: topAnchor), - bottomConstraint: imageView.bottomAnchor.constraint(equalTo: textView.topAnchor)), - centerConstraints: CenterConstraints(centerXConstraint: imageView.centerXAnchor.constraint(equalTo: centerXAnchor), - centerYConstraint: imageView.centerYAnchor.constraint(equalTo: centerYAnchor)), - sizeConstraints: SizeConstraints(widthConstraint: imageView.widthAnchor.constraint(equalToConstant: .zero), - heightConstraint: imageView.heightAnchor.constraint(equalToConstant: .zero))) + let edgeConstraints = EdgeConstraints(leadingConstraint: imageView.leadingAnchor.constraint(equalTo: leadingAnchor), + trailingConstraint: imageView.trailingAnchor.constraint(equalTo: trailingAnchor), + topConstraint: imageView.topAnchor.constraint(equalTo: topAnchor), + bottomConstraint: imageView.bottomAnchor.constraint(equalTo: textView.topAnchor)) + + let centerConstraints = CenterConstraints(centerXConstraint: imageView.centerXAnchor.constraint(equalTo: centerXAnchor), + centerYConstraint: imageView.centerYAnchor.constraint(equalTo: centerYAnchor)) + + let sizeConstraints = SizeConstraints(widthConstraint: imageView.widthAnchor.constraint(equalToConstant: .zero), + heightConstraint: imageView.heightAnchor.constraint(equalToConstant: .zero)) + + return SubviewConstraints(edgeConstraints: edgeConstraints, + centerConstraints: centerConstraints, + sizeConstraints: sizeConstraints) }() public private(set) lazy var textViewTopToSuperviewTopConstraint: NSLayoutConstraint = { textView.topAnchor.constraint(equalTo: topAnchor) }() - public private(set) lazy var textViewBottomToSuperviewBottomConstraint: NSLayoutConstraint = { + public private(set) lazy var textViewToSuperviewBottomConstraint: NSLayoutConstraint = { textView.bottomAnchor.constraint(equalTo: bottomAnchor) }() public private(set) lazy var textViewConstraints: SubviewConstraints = { - SubviewConstraints(edgeConstraints: EdgeConstraints(leadingConstraint: textView.leadingAnchor.constraint(equalTo: leadingAnchor), - trailingConstraint: textView.trailingAnchor.constraint(equalTo: trailingAnchor), - topConstraint: textView.topAnchor.constraint(equalTo: imageView.bottomAnchor), - bottomConstraint: textView.bottomAnchor.constraint(lessThanOrEqualTo: controlsStackView.topAnchor)), - centerConstraints: CenterConstraints(centerXConstraint: textView.centerXAnchor.constraint(equalTo: centerXAnchor), - centerYConstraint: textView.centerYAnchor.constraint(equalTo: centerYAnchor)), - sizeConstraints: SizeConstraints(widthConstraint: textView.widthAnchor.constraint(equalToConstant: .zero), - heightConstraint: textView.heightAnchor.constraint(equalToConstant: .zero))) + let bottomConstraint = textView.bottomAnchor.constraint(lessThanOrEqualTo: controlsStackView.topAnchor) + + let edgeConstraints = EdgeConstraints(leadingConstraint: textView.leadingAnchor.constraint(equalTo: leadingAnchor), + trailingConstraint: textView.trailingAnchor.constraint(equalTo: trailingAnchor), + topConstraint: textView.topAnchor.constraint(equalTo: imageView.bottomAnchor), + bottomConstraint: bottomConstraint) + + let centerConstraints = CenterConstraints(centerXConstraint: textView.centerXAnchor.constraint(equalTo: centerXAnchor), + centerYConstraint: textView.centerYAnchor.constraint(equalTo: centerYAnchor)) + + let sizeConstraints = SizeConstraints(widthConstraint: textView.widthAnchor.constraint(equalToConstant: .zero), + heightConstraint: textView.heightAnchor.constraint(equalToConstant: .zero)) + + return SubviewConstraints(edgeConstraints: edgeConstraints, + centerConstraints: centerConstraints, + sizeConstraints: sizeConstraints) }() public private(set) lazy var controlsViewConstraints: SubviewConstraints = { - SubviewConstraints(edgeConstraints: EdgeConstraints(leadingConstraint: controlsStackView.leadingAnchor.constraint(equalTo: leadingAnchor), - trailingConstraint: controlsStackView.trailingAnchor.constraint(equalTo: trailingAnchor), - topConstraint: controlsStackView.topAnchor.constraint(equalTo: textView.bottomAnchor), - bottomConstraint: controlsStackView.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor)), - centerConstraints: CenterConstraints(centerXConstraint: controlsStackView.centerXAnchor.constraint(equalTo: centerXAnchor), - centerYConstraint: controlsStackView.centerYAnchor.constraint(equalTo: centerYAnchor)), - sizeConstraints: SizeConstraints(widthConstraint: controlsStackView.widthAnchor.constraint(equalToConstant: .zero), - heightConstraint: controlsStackView.heightAnchor.constraint(equalToConstant: .zero))) + let leadingConstraint = controlsStackView.leadingAnchor.constraint(equalTo: leadingAnchor) + let trailingConstraint = controlsStackView.trailingAnchor.constraint(equalTo: trailingAnchor) + let topConstraint = controlsStackView.topAnchor.constraint(equalTo: textView.bottomAnchor) + let bottomConstraint = controlsStackView.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor) + + let edgeConstraints = EdgeConstraints(leadingConstraint: leadingConstraint, + trailingConstraint: trailingConstraint, + topConstraint: topConstraint, + bottomConstraint: bottomConstraint) + + let centerXConstraint = controlsStackView.centerXAnchor.constraint(equalTo: centerXAnchor) + let centerYConstraint = controlsStackView.centerYAnchor.constraint(equalTo: centerYAnchor) + + let centerConstraints = CenterConstraints(centerXConstraint: centerXConstraint, + centerYConstraint: centerYConstraint) + + let heightConstraint = controlsStackView.heightAnchor.constraint(equalToConstant: .zero) + + let sizeConstraints = SizeConstraints(widthConstraint: controlsStackView.widthAnchor.constraint(equalToConstant: .zero), + heightConstraint: heightConstraint) + + return SubviewConstraints(edgeConstraints: edgeConstraints, + centerConstraints: centerConstraints, + sizeConstraints: sizeConstraints) }() - public var keyboardDidShownObserver: NSObjectProtocol? - public var keyboardDidHiddenObserver: NSObjectProtocol? + private var keyboardDidShownObserver: NSObjectProtocol? + private var keyboardDidHiddenObserver: NSObjectProtocol? open var isImageViewHidden: Bool { imageView.isHidden @@ -82,15 +112,15 @@ open class BasePlaceholderView: BaseInitializableView { controlsStackView.isHidden || controlsStackView.arrangedSubviews.isEmpty } - // MARK: - Deinit - deinit { - if let keyboardDidShownObserver = keyboardDidShownObserver { - NotificationCenter.default.removeObserver(keyboardDidShownObserver) + let notificationCenter = NotificationCenter.default + + if let keyboardDidShownObserver { + notificationCenter.removeObserver(keyboardDidShownObserver) } - if let keyboardDidHiddenObserver = keyboardDidHiddenObserver { - NotificationCenter.default.removeObserver(keyboardDidHiddenObserver) + if let keyboardDidHiddenObserver { + notificationCenter.removeObserver(keyboardDidHiddenObserver) } } @@ -146,25 +176,19 @@ open class BasePlaceholderView: BaseInitializableView { // MARK: - Open methods - open func applyBaseStyle(style: BasePlaceholderStyle & ViewAppearance>) { + open func applyBase(style: BasePlaceholderStyle & ViewAppearance>) { textView.configure(with: style.titleSubtitle) controlsStackView.axis = style.controlsViewAxis style.buttonsStyles.forEach { let button = StatefulButton(type: .custom) - button.set(titles: $0.titles) - button.set(images: $0.images) - button.configureStatefulButton(appearance: $0.appearance) - - if let action = $0.action { - button.addTarget(action.target, action: action.action, for: action.event) - } + button.apply(style: $0) controlsStackView.addArrangedSubview(button) } - configureAppearance(appearance: style.appearance) + configureBase(appearance: style.appearance) } open func addAction(_ action: UIButton.Action, @@ -175,10 +199,10 @@ open class BasePlaceholderView: BaseInitializableView { button?.addTarget(action.target, action: action.action, for: action.event) } - open func configureAppearance(appearance: BaseAppearance) { - configureImageViewLayout(layout: appearance.imageViewAppearance.layout) - configureTextViewLayout(layout: appearance.textViewAppearance.layout) - configureControlsViewLayout(layout: appearance.controlsViewAppearance.layout) + open func configureBase(appearance: BaseAppearance) { + configureImageView(layout: appearance.imageViewAppearance.layout) + configureTextView(layout: appearance.textViewAppearance.layout) + configureControlsView(layout: appearance.controlsViewAppearance.layout) configureUIView(appearance: appearance) textView.configure(appearance: appearance.textViewAppearance) @@ -195,7 +219,7 @@ open class BasePlaceholderView: BaseInitializableView { // MARK: - Private methods - private func configureImageViewLayout(layout: WrappedViewLayout) { + private func configureImageView(layout: WrappedViewLayout) { guard !isImageViewHidden else { imageViewConstraints.deactivate() return @@ -204,17 +228,17 @@ open class BasePlaceholderView: BaseInitializableView { imageViewConstraints.update(from: layout) } - private func configureTextViewLayout(layout: WrappedViewLayout) { + private func configureTextView(layout: WrappedViewLayout) { textViewConstraints.edgeConstraints.topConstraint.isActive = !isImageViewHidden textViewTopToSuperviewTopConstraint.isActive = isImageViewHidden textViewConstraints.edgeConstraints.bottomConstraint.isActive = !isControlsViewHidden - textViewBottomToSuperviewBottomConstraint.isActive = isControlsViewHidden + textViewToSuperviewBottomConstraint.isActive = isControlsViewHidden textViewConstraints.update(from: layout) } - private func configureControlsViewLayout(layout: SpacedWrappedViewLayout) { + private func configureControlsView(layout: SpacedWrappedViewLayout) { guard !isControlsViewHidden else { controlsViewConstraints.deactivate() return diff --git a/TIUIElements/Sources/Views/Placeholder/Views/DefaultPlaceholderView.swift b/TIUIElements/Sources/Views/Placeholder/Views/DefaultPlaceholderView.swift index d86a2793..7cebb901 100644 --- a/TIUIElements/Sources/Views/Placeholder/Views/DefaultPlaceholderView.swift +++ b/TIUIElements/Sources/Views/Placeholder/Views/DefaultPlaceholderView.swift @@ -32,7 +32,7 @@ public final class DefaultPlaceholderView: BasePlaceholderView, App public func apply(style: DefaultPlaceholderStyle) { imageView.image = style.image - super.applyBaseStyle(style: style) + super.applyBase(style: style) guard let image = imageView.image else { return @@ -44,14 +44,14 @@ public final class DefaultPlaceholderView: BasePlaceholderView, App // MARK: - AppearanceConfigurable public func configure(appearance: Appearance) { - configureAppearance(appearance: appearance) + configureBase(appearance: appearance) } } // MARK: - Default appearance model -extension DefaultPlaceholderView { - public final class Appearance: BaseAppearance, ViewAppearance { +public extension DefaultPlaceholderView { + final class Appearance: BaseAppearance, ViewAppearance { public static var defaultAppearance: Self { .init() } diff --git a/TIUIElements/Sources/Views/StatefulButton/StatefulButton+ApplyStyle.swift b/TIUIElements/Sources/Views/StatefulButton/StatefulButton+ApplyStyle.swift new file mode 100644 index 00000000..29effcbf --- /dev/null +++ b/TIUIElements/Sources/Views/StatefulButton/StatefulButton+ApplyStyle.swift @@ -0,0 +1,33 @@ +// +// Copyright (c) 2023 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 StatefulButton { + func apply(style: BaseButtonStyle) { + set(titles: style.titles) + set(images: style.images) + configureStatefulButton(appearance: style.appearance) + + if let action = style.action { + addTarget(action.target, action: action.action, for: action.event) + } + } +} diff --git a/TIUIElements/Sources/Wrappers/Constraints/CenterConstraints.swift b/TIUIElements/Sources/Wrappers/Constraints/CenterConstraints.swift index 6aace408..5f55c07d 100644 --- a/TIUIElements/Sources/Wrappers/Constraints/CenterConstraints.swift +++ b/TIUIElements/Sources/Wrappers/Constraints/CenterConstraints.swift @@ -23,8 +23,8 @@ import UIKit public struct CenterConstraints: ConstraintsSet { - public let centerXConstraint: NSLayoutConstraint - public let centerYConstraint: NSLayoutConstraint + public var centerXConstraint: NSLayoutConstraint + public var centerYConstraint: NSLayoutConstraint // MARK: - ConstraintsSet diff --git a/TIUIElements/Sources/Wrappers/Constraints/EdgeConstraints.swift b/TIUIElements/Sources/Wrappers/Constraints/EdgeConstraints.swift index d6278aa6..06afa29a 100644 --- a/TIUIElements/Sources/Wrappers/Constraints/EdgeConstraints.swift +++ b/TIUIElements/Sources/Wrappers/Constraints/EdgeConstraints.swift @@ -23,10 +23,10 @@ import UIKit public struct EdgeConstraints: ConstraintsSet { - public let leadingConstraint: NSLayoutConstraint - public let trailingConstraint: NSLayoutConstraint - public let topConstraint: NSLayoutConstraint - public let bottomConstraint: NSLayoutConstraint + public var leadingConstraint: NSLayoutConstraint + public var trailingConstraint: NSLayoutConstraint + public var topConstraint: NSLayoutConstraint + public var bottomConstraint: NSLayoutConstraint // MARK: - ConstraintsSet diff --git a/TIUIElements/Sources/Wrappers/Constraints/NSLayoutConstraint+SetActiveOrDeactivate.swift b/TIUIElements/Sources/Wrappers/Constraints/NSLayoutConstraint+SetActiveOrDeactivate.swift index 4929b2c3..3b5058c0 100644 --- a/TIUIElements/Sources/Wrappers/Constraints/NSLayoutConstraint+SetActiveOrDeactivate.swift +++ b/TIUIElements/Sources/Wrappers/Constraints/NSLayoutConstraint+SetActiveOrDeactivate.swift @@ -22,7 +22,7 @@ import UIKit.NSLayoutConstraint -extension NSLayoutConstraint { +public extension NSLayoutConstraint { func setActiveConstantOrDeactivate(constant: CGFloat) { if constant.isFinite { self.constant = constant diff --git a/TIUIElements/Sources/Wrappers/Constraints/SizeConstraints.swift b/TIUIElements/Sources/Wrappers/Constraints/SizeConstraints.swift index d65caae7..178b42d4 100644 --- a/TIUIElements/Sources/Wrappers/Constraints/SizeConstraints.swift +++ b/TIUIElements/Sources/Wrappers/Constraints/SizeConstraints.swift @@ -23,8 +23,8 @@ import UIKit public struct SizeConstraints: ConstraintsSet { - public let widthConstraint: NSLayoutConstraint - public let heightConstraint: NSLayoutConstraint + public var widthConstraint: NSLayoutConstraint + public var heightConstraint: NSLayoutConstraint // MARK: - ConstraintsSet diff --git a/TIUIElements/Sources/Wrappers/Constraints/SubviewConstraints.swift b/TIUIElements/Sources/Wrappers/Constraints/SubviewConstraints.swift index 64372a50..5111d28d 100644 --- a/TIUIElements/Sources/Wrappers/Constraints/SubviewConstraints.swift +++ b/TIUIElements/Sources/Wrappers/Constraints/SubviewConstraints.swift @@ -35,8 +35,8 @@ public struct SubviewConstraints: ConstraintsSet { } public init(edgeConstraints: EdgeConstraints, - centerConstraints: CenterConstraints, - sizeConstraints: SizeConstraints) { + centerConstraints: CenterConstraints, + sizeConstraints: SizeConstraints) { self.edgeConstraints = edgeConstraints self.centerConstraints = centerConstraints @@ -47,21 +47,16 @@ public struct SubviewConstraints: ConstraintsSet { centerConstraints.update(offset: centerOffset) sizeConstraints.update(from: size) edgeConstraints.update(from: insets) - - for verticalConstraint in edgeConstraints.vertical { - verticalConstraint.isActive = !centerOffset.vertical.isFinite - } - - for horizontalConstraint in edgeConstraints.horizontal { - horizontalConstraint.isActive = !centerOffset.horizontal.isFinite - } } // MARK: - WrappedViewLayout shortcut - func update(from layout: some WrappedViewLayout) { + @discardableResult + public func update(from layout: some WrappedViewLayout) -> Self { update(insets: layout.insets, size: layout.size, centerOffset: layout.centerOffset) + + return self } } diff --git a/TIUIElements/Sources/Wrappers/Containers/CollectionTableViewCell.swift b/TIUIElements/Sources/Wrappers/Containers/CollectionTableViewCell.swift index 43c2aed1..7a24cc3b 100644 --- a/TIUIElements/Sources/Wrappers/Containers/CollectionTableViewCell.swift +++ b/TIUIElements/Sources/Wrappers/Containers/CollectionTableViewCell.swift @@ -31,7 +31,7 @@ open class CollectionTableViewCell: ContainerT if #available(iOS 16, *) { contentSizeObservation = wrappedView.observe(\.contentSize, - options: [.new]) { collectionView, change in + options: [.new]) { collectionView, change in if let contentSize = change.newValue, !contentSize.equalTo(collectionView.bounds.size) { self.invalidateIntrinsicContentSize() @@ -52,12 +52,12 @@ open class CollectionTableViewCell: ContainerT } let cachedCollectionFrame = wrappedView.frame - wrappedView.frame.size.width = targetSize.width - contentInsets.left - contentInsets.right + wrappedView.frame.size.width = max(targetSize.width - wrappedContentInsets.horizontal(onNan: .zero), .zero) let collectionContentHeight = wrappedView.collectionViewLayout.collectionViewContentSize.height wrappedView.frame = cachedCollectionFrame return CGSize(width: targetSize.width, - height: collectionContentHeight + contentInsets.top + contentInsets.bottom) + height: collectionContentHeight + wrappedContentInsets.vertical(onNan: .zero)) } // MARK: - Open methods diff --git a/TIUIElements/Sources/Wrappers/Containers/ContainerCollectionViewCell.swift b/TIUIElements/Sources/Wrappers/Containers/ContainerCollectionViewCell.swift index 983a88da..85b484fb 100644 --- a/TIUIElements/Sources/Wrappers/Containers/ContainerCollectionViewCell.swift +++ b/TIUIElements/Sources/Wrappers/Containers/ContainerCollectionViewCell.swift @@ -26,24 +26,24 @@ import TIUIKitCore open class ContainerCollectionViewCell: UICollectionViewCell, InitializableViewProtocol, WrappedViewHolder { public var callbacks: [ViewCallbacks] = [] - + // MARK: - WrappedViewHolder public private(set) lazy var wrappedView = createView() - public var contentInsets: UIEdgeInsets = .zero { + public var wrappedContentInsets: UIEdgeInsets = .zero { didSet { update(subviewConstraints: subviewContraints) } } - public var contentSize: CGSize = .infinity { + public var wrappedContentSize: CGSize = .infinity { didSet { update(subviewConstraints: subviewContraints) } } - public var contentCenterOffset: UIOffset = .nan { + public var wrappedContentCenterOffset: UIOffset = .nan { didSet { update(subviewConstraints: subviewContraints) } @@ -98,6 +98,6 @@ open class ContainerCollectionViewCell: UICollectionViewCell, Init } open func createView() -> View { - return View() + View() } } diff --git a/TIUIElements/Sources/Wrappers/Containers/ContainerScrollView.swift b/TIUIElements/Sources/Wrappers/Containers/ContainerScrollView.swift new file mode 100644 index 00000000..29e353ee --- /dev/null +++ b/TIUIElements/Sources/Wrappers/Containers/ContainerScrollView.swift @@ -0,0 +1,73 @@ +// +// Copyright (c) 2023 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 UIKit + +open class ContainerScrollView: BaseInitializeableScrollView, WrappedViewHolder { + + // MARK: - WrappedViewHolder + + public private(set) lazy var wrappedView = createView() + + public var wrappedContentInsets: UIEdgeInsets = .zero { + didSet { + update(subviewConstraints: subviewContraints) + } + } + + public var wrappedContentSize: CGSize = .infinity { + didSet { + update(subviewConstraints: subviewContraints) + } + } + + public var wrappedContentCenterOffset: UIOffset = .nan { + didSet { + update(subviewConstraints: subviewContraints) + } + } + + private lazy var subviewContraints: SubviewConstraints = { + configureWrappedViewLayout() + }() + + open func createView() -> ContentView { + ContentView() + } + + // MARK: - InitializableViewProtocol + + open override func addViews() { + super.addViews() + + addSubview(wrappedView) + } +} + +extension ContainerScrollView: AppearanceConfigurable where View: AppearanceConfigurable, + View.Appearance: WrappedViewAppearance { + + public func configure(appearance: DefaultWrappedViewHolderAppearance) { + configureWrappedViewHolder(appearance: appearance) + } +} diff --git a/TIUIElements/Sources/Wrappers/Containers/ContainerTableViewCell.swift b/TIUIElements/Sources/Wrappers/Containers/ContainerTableViewCell.swift index fc2a93f9..233911c2 100644 --- a/TIUIElements/Sources/Wrappers/Containers/ContainerTableViewCell.swift +++ b/TIUIElements/Sources/Wrappers/Containers/ContainerTableViewCell.swift @@ -24,24 +24,24 @@ import UIKit import TIUIKitCore open class ContainerTableViewCell: BaseInitializableCell, WrappedViewHolder { - + // MARK: - WrappedViewHolder public private(set) lazy var wrappedView = createView() - public var contentInsets: UIEdgeInsets = .zero { + public var wrappedContentInsets: UIEdgeInsets = .zero { didSet { update(subviewConstraints: subviewContraints) } } - public var contentSize: CGSize = .infinity { + public var wrappedContentSize: CGSize = .infinity { didSet { update(subviewConstraints: subviewContraints) } } - public var contentCenterOffset: UIOffset = .nan { + public var wrappedContentCenterOffset: UIOffset = .nan { didSet { update(subviewConstraints: subviewContraints) } @@ -66,14 +66,7 @@ open class ContainerTableViewCell: BaseInitializableCell, WrappedV } open func createView() -> View { - return View() - } - - // MARK: - Open methods - - public func configureContainerTableViewCell(appearance: BaseWrappedViewHolderAppearance) { - updateContentLayout(from: appearance.subviewAppearance.layout) - configureUIView(appearance: appearance) + View() } } @@ -82,8 +75,7 @@ open class ContainerTableViewCell: BaseInitializableCell, WrappedV extension ContainerTableViewCell: AppearanceConfigurable where View: AppearanceConfigurable, View.Appearance: WrappedViewAppearance { - public func configure(appearance: DefaultWrappedViewHolderAppearance) { - configureContainerTableViewCell(appearance: appearance) - wrappedView.configure(appearance: appearance.subviewAppearance) + public func configure(appearance: DefaultWrappedViewHolderAppearance) { + configureWrappedViewHolder(appearance: appearance) } } diff --git a/TIUIElements/Sources/Wrappers/Containers/ContainerView.swift b/TIUIElements/Sources/Wrappers/Containers/ContainerView.swift index e3366378..272ad2e4 100644 --- a/TIUIElements/Sources/Wrappers/Containers/ContainerView.swift +++ b/TIUIElements/Sources/Wrappers/Containers/ContainerView.swift @@ -23,23 +23,23 @@ import TIUIKitCore import UIKit -public final class ContainerView: BaseInitializableView, WrappedViewHolder { +open class ContainerView: BaseInitializableView, WrappedViewHolder { public private(set) lazy var wrappedView = View() - public var contentInsets: UIEdgeInsets = .zero { + public var wrappedContentInsets: UIEdgeInsets = .zero { didSet { update(subviewConstraints: subviewContraints) } } - public var contentSize: CGSize = .infinity { + public var wrappedContentSize: CGSize = .infinity { didSet { update(subviewConstraints: subviewContraints) } } - public var contentCenterOffset: UIOffset = .nan { + public var wrappedContentCenterOffset: UIOffset = .nan { didSet { update(subviewConstraints: subviewContraints) } @@ -80,8 +80,6 @@ extension ContainerView: AppearanceConfigurable where View: AppearanceConfigurab public typealias Appearance = UIView.DefaultWrappedViewHolderAppearance public func configure(appearance: Appearance) { - wrappedView.configure(appearance: appearance.subviewAppearance) - configureUIView(appearance: appearance) - updateContentLayout(from: appearance.subviewAppearance.layout) + configureWrappedViewHolder(appearance: appearance) } } diff --git a/TIUIElements/Sources/Wrappers/Containers/ReusableCollectionContainerView.swift b/TIUIElements/Sources/Wrappers/Containers/ReusableCollectionContainerView.swift index 9f62832c..af087046 100644 --- a/TIUIElements/Sources/Wrappers/Containers/ReusableCollectionContainerView.swift +++ b/TIUIElements/Sources/Wrappers/Containers/ReusableCollectionContainerView.swift @@ -23,26 +23,28 @@ import UIKit import TIUIKitCore -open class ReusableCollectionContainerView: UICollectionReusableView, InitializableViewProtocol, WrappedViewHolder { +open class ReusableCollectionContainerView: UICollectionReusableView, + InitializableViewProtocol, + WrappedViewHolder { public var callbacks: [ViewCallbacks] = [] // MARK: - WrappedViewHolder - + public private(set) lazy var wrappedView = createView() - public var contentInsets: UIEdgeInsets = .zero { + public var wrappedContentInsets: UIEdgeInsets = .zero { didSet { update(subviewConstraints: subviewContraints) } } - public var contentSize: CGSize = .infinity { + public var wrappedContentSize: CGSize = .infinity { didSet { update(subviewConstraints: subviewContraints) } } - public var contentCenterOffset: UIOffset = .nan { + public var wrappedContentCenterOffset: UIOffset = .nan { didSet { update(subviewConstraints: subviewContraints) } @@ -97,6 +99,6 @@ open class ReusableCollectionContainerView: UICollectionReusableVi } open func createView() -> View { - return View() + View() } } diff --git a/TIUIElements/Sources/Wrappers/Extensions/WrappedViewHolder+AppearanceConfigurable.swift b/TIUIElements/Sources/Wrappers/Extensions/WrappedViewHolder+AppearanceConfigurable.swift new file mode 100644 index 00000000..5fcd2186 --- /dev/null +++ b/TIUIElements/Sources/Wrappers/Extensions/WrappedViewHolder+AppearanceConfigurable.swift @@ -0,0 +1,42 @@ +// +// Copyright (c) 2023 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 UIKit.UIView + +public extension WrappedViewHolder { + func configureBaseWrappedViewHolder(appearance: some UIView.BaseWrappedViewHolderAppearance< + some WrappedViewAppearance, + some ViewLayout>) { + updateContentLayout(from: appearance.subviewAppearance.layout) + contentView.configureUIView(appearance: appearance) + } +} + +public extension WrappedViewHolder where View: AppearanceConfigurable, View.Appearance: WrappedViewAppearance { + func configureWrappedViewHolder(appearance: some UIView.BaseWrappedViewHolderAppearance< + View.Appearance, + some ViewLayout>) { + configureBaseWrappedViewHolder(appearance: appearance) + wrappedView.configure(appearance: appearance.subviewAppearance) + } +} diff --git a/TIUIElements/Sources/Wrappers/Protocols/WrappedViewHolder.swift b/TIUIElements/Sources/Wrappers/Protocols/WrappedViewHolder.swift index 54b6dd50..01ce6d97 100644 --- a/TIUIElements/Sources/Wrappers/Protocols/WrappedViewHolder.swift +++ b/TIUIElements/Sources/Wrappers/Protocols/WrappedViewHolder.swift @@ -28,9 +28,10 @@ public protocol WrappedViewHolder: AnyObject { var wrappedView: View { get } var contentView: UIView { get } - var contentInsets: UIEdgeInsets { get set } - var contentSize: CGSize { get set } - var contentCenterOffset: UIOffset { get set } + + var wrappedContentInsets: UIEdgeInsets { get set } + var wrappedContentSize: CGSize { get set } + var wrappedContentCenterOffset: UIOffset { get set } } public extension WrappedViewHolder { @@ -65,17 +66,17 @@ public extension WrappedViewHolder { // MARK: - SubviewConstraints shortcut func update(subviewConstraints: SubviewConstraints) { - subviewConstraints.update(insets: contentInsets, - size: contentSize, - centerOffset: contentCenterOffset) + subviewConstraints.update(insets: wrappedContentInsets, + size: wrappedContentSize, + centerOffset: wrappedContentCenterOffset) } // MARK: - WrappedViewLayout shortcut func updateContentLayout(from layout: some WrappedViewLayout) { - contentInsets = layout.insets - contentSize = layout.size - contentCenterOffset = layout.centerOffset + wrappedContentInsets = layout.insets + wrappedContentSize = layout.size + wrappedContentCenterOffset = layout.centerOffset } } diff --git a/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.playground/Pages/Placeholder.xcplaygroundpage/Contents.swift b/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.playground/Pages/Placeholder.xcplaygroundpage/Contents.swift index 46138048..b29ba3f6 100644 --- a/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.playground/Pages/Placeholder.xcplaygroundpage/Contents.swift +++ b/TIUIElements/TIUIElements.app/Contents/MacOS/TIUIElements.playground/Pages/Placeholder.xcplaygroundpage/Contents.swift @@ -57,10 +57,10 @@ let customStyle = DefaultPlaceholderStyle( }, buttonsStyles: [ .init(titles: [.normal: "Reload"], appearance: .init(stateAppearances: [ - .normal: UIButton.DefaultAppearance(textAttributes: .init(font: .systemFont(ofSize: 20), - color: .black, - alignment: .natural, - isMultiline: false)) + .normal: UIButton.DefaultStateAppearance(textAttributes: .init(font: .systemFont(ofSize: 20), + color: .black, + alignment: .natural, + isMultiline: false)) ])) ]) diff --git a/TIUIElements/TIUIElements.podspec b/TIUIElements/TIUIElements.podspec index 103c020f..979a0bfc 100644 --- a/TIUIElements/TIUIElements.podspec +++ b/TIUIElements/TIUIElements.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIUIElements' - s.version = '1.50.0' + s.version = '1.51.0' s.summary = 'Bunch of useful protocols and views.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIUIKitCore/Sources/Extensions/UIKit/UIEdgeInsets+Extensions.swift b/TIUIKitCore/Sources/Extensions/UIKit/UIEdgeInsets+Extensions.swift index 8eb4257c..4e14ff55 100644 --- a/TIUIKitCore/Sources/Extensions/UIKit/UIEdgeInsets+Extensions.swift +++ b/TIUIKitCore/Sources/Extensions/UIKit/UIEdgeInsets+Extensions.swift @@ -26,28 +26,40 @@ public extension UIEdgeInsets { // MARK: - Factory methods + static var nan: Self { + .edges(.nan) + } + static func edges(_ insets: CGFloat) -> UIEdgeInsets { .init(top: insets, left: insets, bottom: insets, right: insets) } static func horizontal(_ insets: CGFloat) -> UIEdgeInsets { - .init(top: .zero, left: insets, bottom: .zero, right: insets) + .init(top: .nan, left: insets, bottom: .nan, right: insets) } static func vertical(_ insets: CGFloat) -> UIEdgeInsets { - .init(top: insets, left: .zero, bottom: insets, right: .zero) + .init(top: insets, left: .nan, bottom: insets, right: .nan) } - static func horizontal(left: CGFloat = .zero, right: CGFloat = .zero) -> UIEdgeInsets { - .init(top: .zero, left: left, bottom: .zero, right: right) + static func horizontal(left: CGFloat = .nan, right: CGFloat = .nan) -> UIEdgeInsets { + .init(top: .nan, left: left, bottom: .nan, right: right) } - static func vertical(top: CGFloat = .zero, bottom: CGFloat = .zero) -> UIEdgeInsets { - .init(top: top, left: .zero, bottom: bottom, right: .zero) + static func vertical(top: CGFloat = .nan, bottom: CGFloat = .nan) -> UIEdgeInsets { + .init(top: top, left: .nan, bottom: bottom, right: .nan) } // MARK: - Instance methods + func vertical(onNan defaultValue: CGFloat = .nan) -> CGFloat { + add(\.top, to: \.bottom, of: self, onNan: defaultValue) + } + + func horizontal(onNan defaultValue: CGFloat = .nan) -> CGFloat { + add(\.left, to: \.right, of: self, onNan: defaultValue) + } + func horizontal(_ insets: CGFloat) -> UIEdgeInsets { .init(top: top, left: insets, bottom: bottom, right: insets) } @@ -56,11 +68,38 @@ public extension UIEdgeInsets { .init(top: insets, left: left, bottom: insets, right: right) } - func horizontal(left: CGFloat = .zero, right: CGFloat = .zero) -> UIEdgeInsets { + func horizontal(left: CGFloat = .nan, right: CGFloat = .nan) -> UIEdgeInsets { .init(top: top, left: left, bottom: bottom, right: right) } - func vertical(top: CGFloat = .zero, bottom: CGFloat = .zero) -> UIEdgeInsets { + func vertical(top: CGFloat = .nan, bottom: CGFloat = .nan) -> UIEdgeInsets { .init(top: top, left: left, bottom: bottom, right: right) } + + func replacingNan(with defaultValue: CGFloat) -> Self { + .init(top: top.isFinite ? top : defaultValue, + left: left.isFinite ? left : defaultValue, + bottom: bottom.isFinite ? bottom : defaultValue, + right: right.isFinite ? right : defaultValue) + } + + func add(_ lhsKeyPath: KeyPath, + to rhsKeyPath: KeyPath, + of rhs: Self, + onNan defaultValue: CGFloat = .nan) -> CGFloat { + + let lhsValue = self[keyPath: lhsKeyPath] + let rhsValue = rhs[keyPath: rhsKeyPath] + + switch (lhsValue.isFinite, rhsValue.isFinite) { + case (true, true): + return lhsValue + rhsValue + case (true, false): + return lhsValue + case (false, true): + return rhsValue + case (false, false): + return defaultValue + } + } } diff --git a/TIUIKitCore/TIUIKitCore.podspec b/TIUIKitCore/TIUIKitCore.podspec index 098ae299..1e6dc19f 100644 --- a/TIUIKitCore/TIUIKitCore.podspec +++ b/TIUIKitCore/TIUIKitCore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIUIKitCore' - s.version = '1.50.0' + s.version = '1.51.0' s.summary = 'Core UI elements: protocols, views and helpers.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIWebView/TIWebView.podspec b/TIWebView/TIWebView.podspec index 77cfa44a..7ef71d3e 100644 --- a/TIWebView/TIWebView.podspec +++ b/TIWebView/TIWebView.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIWebView' - s.version = '1.50.0' + s.version = '1.51.0' s.summary = 'Universal web view API' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIYandexMapUtils/TIYandexMapUtils.podspec b/TIYandexMapUtils/TIYandexMapUtils.podspec index 0e3d3db0..5bd52978 100644 --- a/TIYandexMapUtils/TIYandexMapUtils.podspec +++ b/TIYandexMapUtils/TIYandexMapUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIYandexMapUtils' - s.version = '1.50.0' + s.version = '1.51.0' s.summary = 'Set of helpers for map objects clustering and interacting using Yandex Maps SDK.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/docs/tibottomsheet/tibottomsheet.md b/docs/tibottomsheet/tibottomsheet.md new file mode 100644 index 00000000..2e7e2173 --- /dev/null +++ b/docs/tibottomsheet/tibottomsheet.md @@ -0,0 +1,108 @@ + +# TIBottomSheet + + TIBottomSheet содержить базовую реализацию модального котроллера и немного видоизмененную библиотеку PanModal. + +## Базовый контроллер + + Для создания модального котроллера можно унаследоваться от `BaseModalViewController`. Данный клас принимает два generic типа: тип основного контента, тип контента футера. + +```swift +import TIBottomSheet +import UIKit + +class EmptyViewController: BaseModalViewController { } +``` + +## Обертка вокруг существующего контроллера + + Может быть такое, что из уже существующего контроллера нужно сделать модальное окно. С этим может помочь обертка `BaseModalWrapperViewController`. Данный контроллер является наследником BaseModalViewController, что позволяет его настраивать так же, как и базовый модальный котроллер + +```swift +import TIUIKitCore + +final class OldMassiveViewController: BaseInitializableViewController { + // some implementation +} + +typealias ModalOldMassiveViewController = BaseModalWrapperViewController + +class PresentingViewController: BaseInitializableViewController { + // some implementation + + @objc private func onButtonTapped() { + presentPanModal(ModalOldMassiveViewController()) + } +} +``` + +## Контент модального контроллера + + Модальный котроллер может содержать следующие элементы: `DragView`, `HeaderView`, `FooterView`. Каждый из них является опциональным и без дополнительных настроек не будет показываться. + + DragView - небольшая view, за которую пользователь "держит" модальный контроллер + HeaderView - контейнер, содержащий в себе кнопки назад/закрыть или какие-то другие элементы управления + FooterView - view, располагающаяся внизу контроллера, поверх всего контента (модальный контроллер уже настроен так, чтобы при скролле в самый низ, футер не перекрывал последнюю ячейку) + + Для настройки каждого у котроллера есть свойство `viewControllerAppearance`. Через него будет настраиваться весь контроллер. Однако стоит заметить, что котроллер не будет настраивать передаваимую вью, содержащую основной контент. Стандартно котроллер будет пытаться расположить контент так, чтобы он заполнил все пространство. + + Вот пример настройки внешнего вида так, чтобы был видет dragView и headerView с левой кнопкой: + +```swift +let customViewController = BaseModalViewController() +customViewController.viewControllerAppearance = BaseModalViewController.DefaultAppearance.make { + $0.dragViewState = .presented(.defaultAppearance) + $0.headerViewState = .presented(.make { + $0.layout.size = .fixedHeight(52) + $0.backgroundColor = .white + $0.contentViewState = .buttonLeft(.init(titles: [.normal: "Close"], + appearance: .init(stateAppearances: [ + .normal: .init(backgroundColor: .blue) + ]))) + }) +} +``` + +## "Якори" контроллера + + Раньше для настройки высоты контроллера необходимо было пользоваться свойствами `longFormHeight`, `shortFormHeight`. В базовом контроллере можно лишь передать список точек на которых контроллер должен будет задержаться: + +```swift +let detentsViewController = BaseModalViewController() +detentsViewController.viewControllerAppearance.presentationDetents = [.headerOnly, .height(300), .maxHeight] +``` + + - headerOnly будет сам пытаться вычеслить высоту хедера и dragView, показывая только их + - height(_) будет показывать контроллер на переданной высоте + - maxHeight - вся высота экрана (до safeArea) + + В данный массив не рекомендуется передавать больше 3 значений, т.к. модальное окно все равно сможет занять только 3 положения на экране. + +## DimmedView и PassthroughDimmedView + + Для контроля `DimmedView` (затемняющей view) есть отдельное свойство `dimmedView`. Эти классы позволяют настраивать поведение при тапе в затемнённую область и кастомизировать затемнение под ваши нужды. + +```swift +let shadowViewController = BaseModalViewController() +let dimmedView = PassthroughDimmedView() +dimmedView.hitTestHandlerView = view +dimmedView.configureUIView(appearance: .init(shadow: UIViewShadow(radius: 8, + color: .black, + opacity: 0.3))) + +shadowViewController.dimmedView = dimmedView +``` + +## Контроль закрытия + + `PanModalPresentable` не умеет в настройку закрытия контроллера, делая это самостоятельно через `dismiss(animated:completion:)`. Теперь можно настроить закрытие самостоятельно через свойства: `onTapToDismiss` и `onDragToDismiss`. + +# Взаимодействие с PanModal + + Если нет необходимости или возможности использовать `BaseModalViewController`, вы все так же можете пользоваться протоколом `PanModalRepresentable`. Вот список изменений протокола: + + - Открытие/закрытие модального окна теперь можно настроить с помощью свойств `onTapToDismiss` и `onDragToDismiss` + - Можно настроить промежуточное состояние модального окна с `mediumFormHeight` + - `DimmedView` открыт для наследования и может создаваться в `dimmedView` у + + > Для `BaseModalViewController` все свойства из `PanModalPresentable` все также работают, т.е. вы можете их переопределять, добавлять и изменять по необходимости. diff --git a/docs/tiuielements/placeholder.md b/docs/tiuielements/placeholder.md index 293fb70e..74074b2d 100644 --- a/docs/tiuielements/placeholder.md +++ b/docs/tiuielements/placeholder.md @@ -60,10 +60,10 @@ let customStyle = DefaultPlaceholderStyle( }, buttonsStyles: [ .init(titles: [.normal: "Reload"], appearance: .init(stateAppearances: [ - .normal: UIButton.DefaultAppearance(textAttributes: .init(font: .systemFont(ofSize: 20), - color: .black, - alignment: .natural, - isMultiline: false)) + .normal: UIButton.DefaultStateAppearance(textAttributes: .init(font: .systemFont(ofSize: 20), + color: .black, + alignment: .natural, + isMultiline: false)) ])) ]) diff --git a/project-scripts/ordered_modules_list.txt b/project-scripts/ordered_modules_list.txt index 48d9aa9b..70c9d452 100644 --- a/project-scripts/ordered_modules_list.txt +++ b/project-scripts/ordered_modules_list.txt @@ -20,4 +20,5 @@ TIEcommerce TIWebView TIDeveloperUtils TIDeeplink -TITextProcessing \ No newline at end of file +TITextProcessing +TIBottomSheet diff --git a/project-scripts/push_to_podspecs.sh b/project-scripts/push_to_podspecs.sh index 2bb93d07..0c3a95d1 100755 --- a/project-scripts/push_to_podspecs.sh +++ b/project-scripts/push_to_podspecs.sh @@ -9,14 +9,23 @@ # Required environment variables: # SRCROOT - path to project folder. # +# Optional environment variables: +# MODULE_NAME - single module to push +# # Examples of usage: # SRCROOT=`pwd` ./project-scripts/push_to_podspecs.sh # -for module_name in $(cat ${SRCROOT}/project-scripts/ordered_modules_list.txt); do - bundle exec pod repo push https://git.svc.touchin.ru/TouchInstinct/Podspecs ${SRCROOT}/${module_name}/${module_name}.podspec "$@" --allow-warnings - - if [ $? -ne 0 ]; then - exit $? - fi -done +GIT_REPO_PATH="git.svc.touchin.ru/TouchInstinct/Podspecs" + +if [ -z "${MODULE_NAME}" ]; then + for module_name in $(cat ${SRCROOT}/project-scripts/ordered_modules_list.txt); do + bundle exec pod repo push "git@${GIT_REPO_PATH}.git" ${SRCROOT}/${module_name}/${module_name}.podspec "$@" --allow-warnings + + if [ $? -ne 0 ]; then + exit $? + fi + done +else + bundle exec pod repo push "git@${GIT_REPO_PATH}.git" ${SRCROOT}/${MODULE_NAME}/${MODULE_NAME}.podspec "$@" --allow-warnings +fi