feat: completed bottom sheet api

This commit is contained in:
Nikita Semenov 2023-06-13 09:29:53 +03:00
parent c06bb56964
commit 919423ecda
45 changed files with 1061 additions and 59 deletions

View File

@ -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.

View File

@ -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

View File

@ -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' }

View File

@ -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' }

View File

@ -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)
}
}
}

View File

@ -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
}
}

View File

@ -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)
}
}
}

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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)
}
}
}

View File

@ -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] {
[]
}

View File

@ -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

View File

@ -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' }

View File

@ -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' }

View File

@ -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' }

View File

@ -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' }

View File

@ -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' }

View File

@ -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' }

View File

@ -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' }

View File

@ -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' }

View File

@ -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' }

View File

@ -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' }

View File

@ -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' }

View File

@ -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' }

View File

@ -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' }

View File

@ -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))
}
}

View File

@ -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' }

View File

@ -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' }

View File

@ -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' }

View File

@ -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 }
}
}

View File

@ -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)
}
}

View File

@ -23,7 +23,7 @@
import TIUIKitCore
import UIKit
extension WrappedViewLayout {
public extension WrappedViewLayout {
func setupSize(widthConstraint: NSLayoutConstraint?,
heightConstraint: NSLayoutConstraint?) {

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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)
}
}

View File

@ -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' }

View File

@ -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 {

View File

@ -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' }

View File

@ -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' }

View File

@ -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' }