feat: completed bottom sheet api
This commit is contained in:
parent
c06bb56964
commit
919423ecda
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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<UIView.DefaultWrappedLayout> {
|
||||
|
||||
public var dragViewState: DragView.State
|
||||
public var headerViewState: ModalHeaderView.State
|
||||
public var footerViewState: ModalFooterView<FooterContentView>.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<FooterContentView>.State = .hidden) {
|
||||
|
||||
self.dragViewState = dragViewState
|
||||
self.headerViewState = headerViewState
|
||||
self.footerViewState = footerViewState
|
||||
|
||||
super.init(layout: layout, backgroundColor: backgroundColor, border: border, shadow: shadow)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ContentView: UIView,
|
||||
FooterContentView: UIView>: 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<FooterContentView> {
|
||||
ModalFooterView<FooterContentView>()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<UIView.DefaultWrappedLayout>, 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ContentView: UIView> = ContainerView<ContentView>
|
||||
|
||||
public extension ModalFooterView {
|
||||
|
||||
// MARK: - Nested Types
|
||||
|
||||
enum State {
|
||||
case hidden
|
||||
case presented(UIView.BaseWrappedViewHolderAppearance<UIView.DefaultWrappedAppearance, UIView.DefaultWrappedLayout>)
|
||||
}
|
||||
|
||||
func configureAppearance(appearance: UIView.BaseWrappedViewHolderAppearance<UIView.DefaultWrappedAppearance, UIView.DefaultWrappedLayout>) {
|
||||
wrappedView.configureUIView(appearance: appearance.subviewAppearance)
|
||||
configureUIView(appearance: appearance)
|
||||
contentInsets = appearance.subviewAppearance.layout.insets
|
||||
}
|
||||
}
|
||||
|
|
@ -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<UIView.DefaultWrappedLayout>)
|
||||
}
|
||||
|
||||
// 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<UIView.DefaultWrappedLayout>, 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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] {
|
||||
[]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
import TIUIKitCore
|
||||
import UIKit
|
||||
|
||||
extension WrappedViewLayout {
|
||||
public extension WrappedViewLayout {
|
||||
func setupSize(widthConstraint: NSLayoutConstraint?,
|
||||
heightConstraint: NSLayoutConstraint?) {
|
||||
|
||||
|
|
|
|||
|
|
@ -28,12 +28,12 @@ open class BasePlaceholderStyle<Appearance: ViewAppearance> {
|
|||
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
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ public final class DefaultPlaceholderStyle: BasePlaceholderStyle<DefaultPlacehol
|
|||
titleSubtitle: DefaultTitleSubtitleViewModel = .init(),
|
||||
appearance: DefaultPlaceholderView.Appearance = .defaultAppearance,
|
||||
controlsViewAxis: NSLayoutConstraint.Axis = .vertical,
|
||||
buttonsStyles: [PlaceholderButtonStyle] = []) {
|
||||
buttonsStyles: [BaseButtonStyle] = []) {
|
||||
|
||||
self.image = image
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ public protocol PlaceholderStyle: AnyObject {
|
|||
var titleSubtitle: DefaultTitleSubtitleViewModel { get set }
|
||||
var controlsViewAxis: NSLayoutConstraint.Axis { get set }
|
||||
var appearance: PlaceholderAppearance { get set }
|
||||
var buttonsStyles: [PlaceholderButtonStyle] { get set }
|
||||
var buttonsStyles: [BaseButtonStyle] { get set }
|
||||
}
|
||||
|
||||
// MARK: - Builder methods
|
||||
|
|
@ -63,8 +63,8 @@ public extension PlaceholderStyle {
|
|||
return self
|
||||
}
|
||||
|
||||
func withButton(_ builder: ParameterClosure<PlaceholderButtonStyle>) -> Self {
|
||||
let buttonStyle = PlaceholderButtonStyle()
|
||||
func withButton(_ builder: ParameterClosure<BaseButtonStyle>) -> 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)
|
||||
|
|
|
|||
|
|
@ -104,7 +104,10 @@ open class BasePlaceholderImageView<Placeholder: UIView>: 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
|
||||
|
|
|
|||
|
|
@ -143,13 +143,7 @@ open class BasePlaceholderView<ImageView: UIView>: 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<ImageView: UIView>: 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<ImageView: UIView>: BaseInitializableView {
|
|||
}
|
||||
|
||||
if let textViewConstraints = textViewConstraints {
|
||||
configureLayout(layout: layout, constraints: textViewConstraints)
|
||||
UIView.configure(layout: layout, constraints: textViewConstraints)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -216,22 +210,10 @@ open class BasePlaceholderView<ImageView: UIView>: 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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ContentView: UIView>: 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
Loading…
Reference in New Issue