diff --git a/CHANGELOG.md b/CHANGELOG.md index 91b67ce3..3cf83c94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +### 1.46.0 + +- **Added**: `BaseModalViewController` implementing `PanModalPresentable` with additional functionality +- **Updated**: Helper methods for `WrappedLayout` and `UIEdgeInsets` + ### 1.45.0 - **Added**: `SingleValueStorage` implementations + `AppInstallLifetimeSingleValueStorage` for automatically removing keychain items on app reinstall. diff --git a/Package.swift b/Package.swift index 18d25962..942e3c42 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"]), @@ -61,9 +62,13 @@ let package = Package( .target(name: "TIUIKitCore", dependencies: ["TISwiftUtils"], path: "TIUIKitCore/Sources"), .target(name: "TIUIElements", dependencies: ["TIUIKitCore", "TISwiftUtils"], path: "TIUIElements/Sources"), .target(name: "TIWebView", dependencies: ["TIUIKitCore", "TISwiftUtils"], path: "TIWebView/Sources"), + .target(name: "TIBottomSheet", dependencies: ["TIUIElements", "TIUIKitCore", "TISwiftUtils"], path: "TIBottomSheet/Sources"), // MARK: - SwiftUI - .target(name: "TISwiftUICore", dependencies: ["TIUIKitCore", "TISwiftUtils"], path: "TISwiftUICore/Sources"), + .target(name: "TISwiftUICore", + dependencies: ["TIUIKitCore", "TISwiftUtils"], + path: "TISwiftUICore/Sources", + plugins: [.plugin(name: "TISwiftLintPlugin")]), // MARK: - Utils diff --git a/TIAppleMapUtils/TIAppleMapUtils.podspec b/TIAppleMapUtils/TIAppleMapUtils.podspec index 6b1c6fc1..b8434269 100644 --- a/TIAppleMapUtils/TIAppleMapUtils.podspec +++ b/TIAppleMapUtils/TIAppleMapUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIAppleMapUtils' - s.version = '1.45.0' + s.version = '1.46.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 d9783051..be7cf4ad 100644 --- a/TIAuth/TIAuth.podspec +++ b/TIAuth/TIAuth.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIAuth' - s.version = '1.45.0' + s.version = '1.46.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/Sources/BottomSheet/BaseModalViewController+Appearance.swift b/TIBottomSheet/Sources/BottomSheet/BaseModalViewController+Appearance.swift new file mode 100644 index 00000000..53d66d0e --- /dev/null +++ b/TIBottomSheet/Sources/BottomSheet/BaseModalViewController+Appearance.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 TIUIElements +import TIUIKitCore +import UIKit + +extension BaseModalViewController { + + open class BaseAppearance: UIView.BaseAppearance { + + 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, + dragViewState: DragView.State = .hidden, + headerViewState: ModalHeaderView.State = .hidden, + footerViewState: ModalFooterView.State = .hidden) { + + self.dragViewState = dragViewState + self.headerViewState = headerViewState + self.footerViewState = footerViewState + + super.init(layout: layout, backgroundColor: backgroundColor, border: border, shadow: shadow) + } + } +} diff --git a/TIBottomSheet/Sources/BottomSheet/BaseModalViewController.swift b/TIBottomSheet/Sources/BottomSheet/BaseModalViewController.swift new file mode 100644 index 00000000..c30201d7 --- /dev/null +++ b/TIBottomSheet/Sources/BottomSheet/BaseModalViewController.swift @@ -0,0 +1,349 @@ +// +// 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 + +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 var dragViewConstraints: SubviewConstraints? + public var headerViewConstraints: SubviewConstraints? + public var contentViewConstraints: SubviewConstraints? + public var footerViewConstraints: SubviewConstraints? + + public var keyboardDidShownObserver: NSObjectProtocol? + public var keyboardDidHiddenObserver: NSObjectProtocol? + + // MARK: - Modal View Controller Configuration + + open var viewControllerAppearance: BaseAppearance { + .init(backgroundColor: .white) + } + + open var panScrollable: UIScrollView? { + contentView as? UIScrollView + } + + open var presentationDetents: [ModalViewPresentationDetent] { + [.maxHeight] + } + + 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 + } + + // MARK: - Life Cycle + + deinit { + if let keyboardDidShownObserver { + NotificationCenter.default.removeObserver(keyboardDidShownObserver) + } + + if let keyboardDidHiddenObserver { + NotificationCenter.default.removeObserver(keyboardDidHiddenObserver) + } + } + + open override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + adjustScrollViewIfNeeded() + } + + // MARK: - BaseInitializableViewController + + open override func addViews() { + super.addViews() + + view.addSubviews(dragView, headerView, contentView, footerView) + } + + open override func configureLayout() { + super.configureLayout() + + 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 height = getKeyboardHeight(notification) else { + return + } + + if let panScrollable { + if isKeyboardHidden { + adjustScrollViewIfNeeded() + } else { + panScrollable.contentInset = .vertical(bottom: height) + } + + } else { + contentViewConstraints?.bottomConstraint?.constant = isKeyboardHidden ? .zero : height + } + } + + open func adjustScrollViewIfNeeded() { + guard let panScrollable, case let .presented(appearance) = viewControllerAppearance.footerViewState else { + return + } + + let verticalInsets = appearance.layout.insets.vertical + let subviewVerticalInsets = appearance.subviewAppearance.layout.insets.vertical + let totalHeight = getHeight(of: footerView) + verticalInsets + subviewVerticalInsets + + panScrollable.contentInset = .vertical(bottom: totalHeight) + } + + // MARK: - Private Methods + + private func configureDragViewLayout() { + guard case let .presented(appearance) = viewControllerAppearance.dragViewState else { + return + } + + dragView.translatesAutoresizingMaskIntoConstraints = false + + let dragViewConstraints = SubviewConstraints( + centerXConstraint: dragView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + topConstraint: dragView.topAnchor.constraint(equalTo: view.topAnchor), + widthConstraint: dragView.widthAnchor.constraint(equalToConstant: .zero), + heightConstraint: dragView.heightAnchor.constraint(equalToConstant: .zero)) + self.dragViewConstraints = dragViewConstraints + + + UIView.configure(layout: appearance.layout, constraints: dragViewConstraints) + NSLayoutConstraint.activate(dragViewConstraints.centerConstraints) + } + + private func configureHeaderViewLayout() { + guard case let .presented(appearance) = viewControllerAppearance.headerViewState else { + return + } + + headerView.translatesAutoresizingMaskIntoConstraints = false + + let isTopView = dragViewConstraints == nil + let headerViewConstraints = SubviewConstraints( + leadingConstraint: headerView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + topConstraint: headerView.topAnchor.constraint(equalTo: isTopView ? view.topAnchor : dragView.bottomAnchor), + trailingConstraint: headerView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + widthConstraint: headerView.widthAnchor.constraint(equalToConstant: .zero), + heightConstraint: headerView.heightAnchor.constraint(equalToConstant: .zero)) + self.headerViewConstraints = headerViewConstraints + + UIView.configure(layout: appearance.layout, constraints: headerViewConstraints) + } + + private func configureContentViewLayout() { + contentView.translatesAutoresizingMaskIntoConstraints = false + + var topView: UIView + + if headerViewConstraints == nil { + if dragViewConstraints == nil { + topView = view + } else { + topView = dragView + } + + } else { + topView = headerView + } + + let contentViewConstraints = SubviewConstraints( + leadingConstraint: contentView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + topConstraint: contentView.topAnchor.constraint(equalTo: topView.bottomAnchor), + trailingConstraint: contentView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + bottomConstraint: contentView.bottomAnchor.constraint(equalTo: view.bottomAnchor)) + self.contentViewConstraints = contentViewConstraints + + NSLayoutConstraint.activate(contentViewConstraints.constraints) + } + + private func configureFooterViewLayout() { + guard case let .presented(appearance) = viewControllerAppearance.footerViewState else { + return + } + + footerView.translatesAutoresizingMaskIntoConstraints = false + + let footerViewConstraints = SubviewConstraints( + leadingConstraint: footerView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + trailingConstraint: footerView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + bottomConstraint: footerView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + widthConstraint: footerView.widthAnchor.constraint(equalToConstant: .zero), + heightConstraint: footerView.heightAnchor.constraint(equalToConstant: .zero)) + self.footerViewConstraints = footerViewConstraints + + NSLayoutConstraint.deactivate(footerViewConstraints.sizeConstraints) + UIView.configure(layout: appearance.layout, constraints: footerViewConstraints) + } + + 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.configureAppearance(appearance: appearance) + } + } + + private func getSortedDetents() -> [ModalViewPresentationDetent] { + 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 + } + + private func getHeaderViewVerticalInsets() -> CGFloat { + guard case let .presented(appearance) = viewControllerAppearance.headerViewState else { + return .zero + } + + return appearance.layout.insets.vertical + } + + 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/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..c01ad4b9 --- /dev/null +++ b/TIBottomSheet/Sources/BottomSheet/Models/ModalViewPresentationDetent.swift @@ -0,0 +1,66 @@ +// +// 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 + +public struct ModalViewPresentationDetent: Hashable { + + // MARK: - Default Values + + public static var headerOnly: ModalViewPresentationDetent { + ModalViewPresentationDetent(height: CGFloat(Int.min)) + } + + public static func height(_ height: CGFloat) -> ModalViewPresentationDetent { + ModalViewPresentationDetent(height: height) + } + + public static var maxHeight: ModalViewPresentationDetent { + ModalViewPresentationDetent(height: CGFloat(Int.max)) + } + + // 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..1f05d400 --- /dev/null +++ b/TIBottomSheet/Sources/BottomSheet/Views/DragView.swift @@ -0,0 +1,74 @@ +// +// 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: Appearance { + Self().update { dragView in + dragView.backgroundColor = Constants.dragViewBackgroundColor + dragView.border = Constants.dragViewBorder + + dragView.layout { layout in + layout.size = Constants.dragViewSize + layout.insets = .vertical(top: Constants.dragViewTopInset) + } + } + } + } + + // 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..d1f00499 --- /dev/null +++ b/TIBottomSheet/Sources/BottomSheet/Views/ModalFooterView.swift @@ -0,0 +1,43 @@ +// +// 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 typealias ModalFooterView = ContainerView + +public extension ModalFooterView { + + // MARK: - Nested Types + + enum State { + case hidden + case presented(UIView.BaseWrappedViewHolderAppearance) + } + + func configureAppearance(appearance: UIView.BaseWrappedViewHolderAppearance) { + wrappedView.configureUIView(appearance: appearance.subviewAppearance) + configureUIView(appearance: appearance) + contentInsets = appearance.subviewAppearance.layout.insets + } +} diff --git a/TIBottomSheet/Sources/BottomSheet/Views/ModalHeaderView.swift b/TIBottomSheet/Sources/BottomSheet/Views/ModalHeaderView.swift new file mode 100644 index 00000000..4383b8ef --- /dev/null +++ b/TIBottomSheet/Sources/BottomSheet/Views/ModalHeaderView.swift @@ -0,0 +1,181 @@ +// +// 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 none + case buttonLeft(BaseButtonStyle) + case buttonRight(BaseButtonStyle) + case buttons(left: BaseButtonStyle, right: BaseButtonStyle) + case custom(view: UIView, appearance: UIView.BaseWrappedAppearance) + } + + // MARK: - Public properties + + public let leftButton = StatefulButton() + public let rightButton = StatefulButton() + + public var leftButtonConstraints: SubviewConstraints? + public var rightButtonConstraints: SubviewConstraints? + public var customViewConstraints: SubviewConstraints? + + // MARK: - BaseInitializableView + + open override func addViews() { + super.addViews() + + addSubviews(leftButton, rightButton) + } + + open override func configureLayout() { + super.configureLayout() + + [leftButton, rightButton] + .forEach { $0.translatesAutoresizingMaskIntoConstraints = false } + + let leftButtonConstraints = SubviewConstraints( + centerYConstraint: leftButton.centerYAnchor.constraint(equalTo: centerYAnchor), + leadingConstraint: leftButton.leadingAnchor.constraint(equalTo: leadingAnchor), + topConstraint: leftButton.topAnchor.constraint(equalTo: topAnchor), + bottomConstraint: leftButton.bottomAnchor.constraint(equalTo: bottomAnchor)) + + let rightButtonConstraints = SubviewConstraints( + centerYConstraint: rightButton.centerYAnchor.constraint(equalTo: centerYAnchor), + topConstraint: rightButton.topAnchor.constraint(equalTo: topAnchor), + trailingConstraint: rightButton.trailingAnchor.constraint(equalTo: trailingAnchor), + bottomConstraint: rightButton.bottomAnchor.constraint(equalTo: bottomAnchor)) + self.leftButtonConstraints = leftButtonConstraints + self.rightButtonConstraints = rightButtonConstraints + + NSLayoutConstraint.activate(leftButtonConstraints.constraints + rightButtonConstraints.constraints) + } + + // MARK: - AppearanceConfigurable + + open func configure(appearance: Appearance) { + configureUIView(appearance: appearance) + + configureContentView(state: appearance.contentViewState) + } + + open func configureContentView(state: ContentViewState) { + var leftButtonStyle: BaseButtonStyle? + var rightButtonStyle: BaseButtonStyle? + + switch state { + case let .buttonLeft(style): + leftButtonStyle = style + + case let .buttonRight(style): + rightButtonStyle = style + + case let .buttons(left, right): + leftButtonStyle = left + rightButtonStyle = right + + case let .custom(view, appearance): + configureCustomView(view, withLayout: appearance.layout) + view.configureUIView(appearance: appearance) + + default: + break + } + + configure(buttonStyle: leftButtonStyle, forButton: leftButton, constraints: leftButtonConstraints) + configure(buttonStyle: rightButtonStyle, forButton: rightButton, constraints: rightButtonConstraints) + } + + // MARK: - Private methods + + private func configureCustomView(_ view: UIView, withLayout layout: WrappedViewLayout) { + addSubview(view) + + view.translatesAutoresizingMaskIntoConstraints = false + + let customViewConstraints = SubviewConstraints( + centerXConstraint: view.centerXAnchor.constraint(equalTo: centerXAnchor), + centerYConstraint: view.centerYAnchor.constraint(equalTo: centerYAnchor), + leadingConstraint: view.leadingAnchor.constraint(equalTo: leadingAnchor), + topConstraint: view.topAnchor.constraint(equalTo: topAnchor), + trailingConstraint: view.trailingAnchor.constraint(equalTo: trailingAnchor), + bottomConstraint: view.bottomAnchor.constraint(equalTo: bottomAnchor)) + self.customViewConstraints = customViewConstraints + + NSLayoutConstraint.deactivate(customViewConstraints.centerConstraints) + + Self.configure(layout: layout, constraints: customViewConstraints) + } + + private func configure(buttonStyle: BaseButtonStyle?, + forButton button: StatefulButton, + constraints: SubviewConstraints?) { + + guard let buttonStyle else { + button.isHidden = true + return + } + + button.isHidden = false + + if let layout = buttonStyle.appearance[.normal]?.layout, let constraints { + UIView.configure(layout: layout, constraints: constraints) + } + + button.apply(style: buttonStyle) + } +} + +// MARK: - Appearance + +extension ModalHeaderView { + + public final class Appearance: UIView.BaseWrappedAppearance, WrappedViewAppearance { + public static var defaultAppearance: Self { + .init() + } + + public var contentViewState: ContentViewState + + public init(layout: UIView.DefaultWrappedLayout = .defaultLayout, + backgroundColor: UIColor = .clear, + border: UIViewBorder = .init(), + shadow: UIViewShadow? = nil, + contentViewState: ContentViewState = .none) { + + self.contentViewState = contentViewState + + super.init(layout: layout, backgroundColor: backgroundColor, border: border, shadow: shadow) + } + } +} diff --git a/TIBottomSheet/Sources/PanModal/Presentable/PanModalPresentable+Defaults.swift b/TIBottomSheet/Sources/PanModal/Presentable/PanModalPresentable+Defaults.swift index fc9edfa1..d6c7dcd4 100644 --- a/TIBottomSheet/Sources/PanModal/Presentable/PanModalPresentable+Defaults.swift +++ b/TIBottomSheet/Sources/PanModal/Presentable/PanModalPresentable+Defaults.swift @@ -6,6 +6,7 @@ // #if os(iOS) +import TISwiftUtils import UIKit /** @@ -13,6 +14,18 @@ import UIKit */ public extension PanModalPresentable where Self: UIViewController { + var onTapToDismiss: VoidClosure? { + { [weak self] in + self?.dismiss(animated: true) + } + } + + var onDragToDismiss: VoidClosure? { + { [weak self] in + self?.dismiss(animated: true) + } + } + var topOffset: CGFloat { topLayoutOffset } @@ -35,6 +48,10 @@ public extension PanModalPresentable where Self: UIViewController { return .contentHeight(scrollView.contentSize.height) } + var dimmedViewType: DimmedView.AppearanceType { + .opaque + } + var presentationDetents: [ModalViewPresentationDetent] { [] } diff --git a/TIBottomSheet/TIBottomSheet.podspec b/TIBottomSheet/TIBottomSheet.podspec new file mode 100644 index 00000000..473f1079 --- /dev/null +++ b/TIBottomSheet/TIBottomSheet.podspec @@ -0,0 +1,17 @@ +Pod::Spec.new do |s| + s.name = 'TIBottomSheet' + s.version = '1.45.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'] + + s.source_files = s.name + '/Sources/**/*' + +end + diff --git a/TIDeeplink/TIDeeplink.podspec b/TIDeeplink/TIDeeplink.podspec index 2584a307..14e443fe 100644 --- a/TIDeeplink/TIDeeplink.podspec +++ b/TIDeeplink/TIDeeplink.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIDeeplink' - s.version = '1.45.0' + s.version = '1.46.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 688d67f9..67319403 100644 --- a/TIDeveloperUtils/TIDeveloperUtils.podspec +++ b/TIDeveloperUtils/TIDeveloperUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIDeveloperUtils' - s.version = '1.45.0' + s.version = '1.46.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/TIEcommerce.podspec b/TIEcommerce/TIEcommerce.podspec index d843966c..a4726845 100644 --- a/TIEcommerce/TIEcommerce.podspec +++ b/TIEcommerce/TIEcommerce.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIEcommerce' - s.version = '1.45.0' + s.version = '1.46.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 3be78475..d70cc0cb 100644 --- a/TIFoundationUtils/TIFoundationUtils.podspec +++ b/TIFoundationUtils/TIFoundationUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIFoundationUtils' - s.version = '1.45.0' + s.version = '1.46.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 a5e1af69..9b82172a 100644 --- a/TIGoogleMapUtils/TIGoogleMapUtils.podspec +++ b/TIGoogleMapUtils/TIGoogleMapUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIGoogleMapUtils' - s.version = '1.45.0' + s.version = '1.46.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 0b37df22..d4c542f1 100644 --- a/TIKeychainUtils/TIKeychainUtils.podspec +++ b/TIKeychainUtils/TIKeychainUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIKeychainUtils' - s.version = '1.45.0' + s.version = '1.46.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 d47e273a..b6aac309 100644 --- a/TILogging/TILogging.podspec +++ b/TILogging/TILogging.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TILogging' - s.version = '1.45.0' + s.version = '1.46.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 2ce197c2..7290e8cb 100644 --- a/TIMapUtils/TIMapUtils.podspec +++ b/TIMapUtils/TIMapUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIMapUtils' - s.version = '1.45.0' + s.version = '1.46.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 7c885bdd..b4e1ae17 100644 --- a/TIMoyaNetworking/TIMoyaNetworking.podspec +++ b/TIMoyaNetworking/TIMoyaNetworking.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIMoyaNetworking' - s.version = '1.45.0' + s.version = '1.46.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 b00f90fd..d46d7558 100644 --- a/TINetworking/TINetworking.podspec +++ b/TINetworking/TINetworking.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TINetworking' - s.version = '1.45.0' + s.version = '1.46.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 a66652cc..4e18213c 100644 --- a/TINetworkingCache/TINetworkingCache.podspec +++ b/TINetworkingCache/TINetworkingCache.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TINetworkingCache' - s.version = '1.45.0' + s.version = '1.46.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 180d8c99..685cf69c 100644 --- a/TIPagination/TIPagination.podspec +++ b/TIPagination/TIPagination.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIPagination' - s.version = '1.45.0' + s.version = '1.46.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 32478aa0..193415fb 100644 --- a/TISwiftUICore/TISwiftUICore.podspec +++ b/TISwiftUICore/TISwiftUICore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TISwiftUICore' - s.version = '1.45.0' + s.version = '1.46.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 a8c15a3c..de1784b6 100644 --- a/TISwiftUtils/TISwiftUtils.podspec +++ b/TISwiftUtils/TISwiftUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TISwiftUtils' - s.version = '1.45.0' + s.version = '1.46.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 8b3eee32..250c7a03 100644 --- a/TITableKitUtils/TITableKitUtils.podspec +++ b/TITableKitUtils/TITableKitUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TITableKitUtils' - s.version = '1.45.0' + s.version = '1.46.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 2ed02fb1..b75e409e 100644 --- a/TITextProcessing/TITextProcessing.podspec +++ b/TITextProcessing/TITextProcessing.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TITextProcessing' - s.version = '1.45.0' + s.version = '1.46.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/Helpers/SubviewConstraints.swift b/TIUIElements/Sources/Helpers/SubviewConstraints.swift index b02f1e4f..4699cd58 100644 --- a/TIUIElements/Sources/Helpers/SubviewConstraints.swift +++ b/TIUIElements/Sources/Helpers/SubviewConstraints.swift @@ -32,6 +32,25 @@ public struct SubviewConstraints { public var widthConstraint: NSLayoutConstraint? public var heightConstraint: NSLayoutConstraint? + public init(centerXConstraint: NSLayoutConstraint? = nil, + centerYConstraint: NSLayoutConstraint? = nil, + leadingConstraint: NSLayoutConstraint? = nil, + topConstraint: NSLayoutConstraint? = nil, + trailingConstraint: NSLayoutConstraint? = nil, + bottomConstraint: NSLayoutConstraint? = nil, + widthConstraint: NSLayoutConstraint? = nil, + heightConstraint: NSLayoutConstraint? = nil) { + + self.centerXConstraint = centerXConstraint + self.centerYConstraint = centerYConstraint + self.leadingConstraint = leadingConstraint + self.topConstraint = topConstraint + self.trailingConstraint = trailingConstraint + self.bottomConstraint = bottomConstraint + self.widthConstraint = widthConstraint + self.heightConstraint = heightConstraint + } + public var constraints: [NSLayoutConstraint] { [ centerXConstraint, @@ -49,4 +68,9 @@ public struct SubviewConstraints { [widthConstraint, heightConstraint] .compactMap { $0 } } + + public var centerConstraints: [NSLayoutConstraint] { + [centerXConstraint, centerYConstraint] + .compactMap { $0 } + } } diff --git a/TIUIElements/Sources/Views/Helpers/UIView+ApplyLayout.swift b/TIUIElements/Sources/Views/Helpers/UIView+ApplyLayout.swift new file mode 100644 index 00000000..ad0a8bb3 --- /dev/null +++ b/TIUIElements/Sources/Views/Helpers/UIView+ApplyLayout.swift @@ -0,0 +1,38 @@ +// +// 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 + +extension UIView { + public static func configure(layout: WrappedViewLayout, constraints: SubviewConstraints) { + layout.setupCenterYOffset(centerYConstraint: constraints.centerYConstraint, + topConstraint: constraints.topConstraint, + bottomConstraint: constraints.bottomConstraint) + + layout.setupCenterXOffset(centerXConstraint: constraints.centerXConstraint, + leadingConstraint: constraints.leadingConstraint, + trailingConstraint: constraints.trailingConstraint) + + layout.setupSize(widthConstraint: constraints.widthConstraint, heightConstraint: constraints.heightConstraint) + } +} diff --git a/TIUIElements/Sources/Views/Helpers/WrappedViewLayout+Helpers.swift b/TIUIElements/Sources/Views/Helpers/WrappedViewLayout+Helpers.swift index e9881d1b..acc74976 100644 --- a/TIUIElements/Sources/Views/Helpers/WrappedViewLayout+Helpers.swift +++ b/TIUIElements/Sources/Views/Helpers/WrappedViewLayout+Helpers.swift @@ -23,7 +23,7 @@ import TIUIKitCore import UIKit -extension WrappedViewLayout { +public extension WrappedViewLayout { func setupSize(widthConstraint: NSLayoutConstraint?, heightConstraint: NSLayoutConstraint?) { diff --git a/TIUIElements/Sources/Views/Placeholder/Styles/BasePlaceholderStyle.swift b/TIUIElements/Sources/Views/Placeholder/Styles/BasePlaceholderStyle.swift index a4a052b7..50f56c7c 100644 --- a/TIUIElements/Sources/Views/Placeholder/Styles/BasePlaceholderStyle.swift +++ b/TIUIElements/Sources/Views/Placeholder/Styles/BasePlaceholderStyle.swift @@ -28,12 +28,12 @@ open class BasePlaceholderStyle { 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 40346aae..dc1b94e4 100644 --- a/TIUIElements/Sources/Views/Placeholder/Views/BasePlaceholderImageView.swift +++ b/TIUIElements/Sources/Views/Placeholder/Views/BasePlaceholderImageView.swift @@ -104,7 +104,10 @@ open class BasePlaceholderImageView: UIImageView, configureUIView(appearance: appearance) placeholderView.configureUIView(appearance: appearance.subviewAppearance) - configurePlaceholderLayout(layout: appearance.subviewAppearance.layout) + + if let placeholderConstraints { + UIView.configure(layout: appearance.subviewAppearance.layout, constraints: placeholderConstraints) + } } // MARK: - Private methods diff --git a/TIUIElements/Sources/Views/Placeholder/Views/BasePlaceholderView.swift b/TIUIElements/Sources/Views/Placeholder/Views/BasePlaceholderView.swift index 55663664..dd2083e8 100644 --- a/TIUIElements/Sources/Views/Placeholder/Views/BasePlaceholderView.swift +++ b/TIUIElements/Sources/Views/Placeholder/Views/BasePlaceholderView.swift @@ -143,13 +143,7 @@ open class BasePlaceholderView: BaseInitializableView { style.buttonsStyles.forEach { let button = StatefulButton(type: .custom) - button.set(titles: $0.titles) - button.set(images: $0.images) - button.set(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) } @@ -191,7 +185,7 @@ open class BasePlaceholderView: BaseInitializableView { return } - configureLayout(layout: layout, constraints: imageViewConstraints) + UIView.configure(layout: layout, constraints: imageViewConstraints) } private func configureTextViewLayout(layout: WrappedViewLayout) { @@ -206,7 +200,7 @@ open class BasePlaceholderView: BaseInitializableView { } if let textViewConstraints = textViewConstraints { - configureLayout(layout: layout, constraints: textViewConstraints) + UIView.configure(layout: layout, constraints: textViewConstraints) } } @@ -216,22 +210,10 @@ open class BasePlaceholderView: BaseInitializableView { return } - configureLayout(layout: layout, constraints: controlsViewConstraints) + UIView.configure(layout: layout, constraints: controlsViewConstraints) controlsStackView.spacing = layout.spacing } - private func configureLayout(layout: WrappedViewLayout, constraints: SubviewConstraints) { - layout.setupSize(widthConstraint: constraints.widthConstraint, heightConstraint: constraints.heightConstraint) - - layout.setupCenterYOffset(centerYConstraint: constraints.centerYConstraint, - topConstraint: constraints.topConstraint, - bottomConstraint: constraints.bottomConstraint) - - layout.setupCenterXOffset(centerXConstraint: constraints.centerXConstraint, - leadingConstraint: constraints.leadingConstraint, - trailingConstraint: constraints.trailingConstraint) - } - private func getKeyboardHeight(_ notification: Notification) -> CGFloat? { guard let userInfo = notification.userInfo else { return nil diff --git a/TIUIElements/Sources/Views/StatefulButton/StatefulButton+ApplyStyle.swift b/TIUIElements/Sources/Views/StatefulButton/StatefulButton+ApplyStyle.swift index e3ae3027..da7b1f67 100644 --- a/TIUIElements/Sources/Views/StatefulButton/StatefulButton+ApplyStyle.swift +++ b/TIUIElements/Sources/Views/StatefulButton/StatefulButton+ApplyStyle.swift @@ -1,8 +1,33 @@ // -// File.swift -// +// Copyright (c) 2023 Touch Instinct // -// Created by Nikita Semenov on 08.06.2023. +// 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 +extension StatefulButton { + public func apply(style: BaseButtonStyle) { + set(titles: style.titles) + set(images: style.images) + set(appearance: style.appearance) + + if let action = style.action { + addTarget(action.target, action: action.action, for: action.event) + } + } +} diff --git a/TIUIElements/Sources/Wrappers/Containers/ScrollViewWrapper.swift b/TIUIElements/Sources/Wrappers/Containers/ScrollViewWrapper.swift new file mode 100644 index 00000000..d9672ca4 --- /dev/null +++ b/TIUIElements/Sources/Wrappers/Containers/ScrollViewWrapper.swift @@ -0,0 +1,52 @@ +// +// 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 UIKit + +public final class ScrollViewWrapper: UIScrollView { + + private let contentView: ContentView + + public override init(frame: CGRect) { + self.contentView = ContentView() + + super.init(frame: .zero) + + addSubview(contentView) + contentView.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + contentView.leadingAnchor.constraint(equalTo: leadingAnchor), + contentView.trailingAnchor.constraint(equalTo: trailingAnchor), + contentView.topAnchor.constraint(equalTo: topAnchor), + contentView.bottomAnchor.constraint(equalTo: bottomAnchor), + contentView.widthAnchor.constraint(equalTo: widthAnchor) + ]) + } + + required init?(coder: NSCoder) { + contentView = ContentView() + + super.init(coder: coder) + } +} diff --git a/TIUIElements/TIUIElements.podspec b/TIUIElements/TIUIElements.podspec index 7866a8e6..efc65250 100644 --- a/TIUIElements/TIUIElements.podspec +++ b/TIUIElements/TIUIElements.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIUIElements' - s.version = '1.45.0' + s.version = '1.46.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..41d9af39 100644 --- a/TIUIKitCore/Sources/Extensions/UIKit/UIEdgeInsets+Extensions.swift +++ b/TIUIKitCore/Sources/Extensions/UIKit/UIEdgeInsets+Extensions.swift @@ -46,6 +46,16 @@ public extension UIEdgeInsets { .init(top: top, left: .zero, bottom: bottom, right: .zero) } + // MARK: - Computed Properties + + var vertical: CGFloat { + top + bottom + } + + var horizontal: CGFloat { + left + right + } + // MARK: - Instance methods func horizontal(_ insets: CGFloat) -> UIEdgeInsets { diff --git a/TIUIKitCore/TIUIKitCore.podspec b/TIUIKitCore/TIUIKitCore.podspec index dbe86d0c..4dd44d2c 100644 --- a/TIUIKitCore/TIUIKitCore.podspec +++ b/TIUIKitCore/TIUIKitCore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIUIKitCore' - s.version = '1.45.0' + s.version = '1.46.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 c6a1fb90..35a86c34 100644 --- a/TIWebView/TIWebView.podspec +++ b/TIWebView/TIWebView.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIWebView' - s.version = '1.45.0' + s.version = '1.46.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 757a8b3c..fd9f99bc 100644 --- a/TIYandexMapUtils/TIYandexMapUtils.podspec +++ b/TIYandexMapUtils/TIYandexMapUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIYandexMapUtils' - s.version = '1.45.0' + s.version = '1.46.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' }