feature/bottom-sheet #11

Merged
ivan.smolin merged 15 commits from feature/bottom-sheet into master 2023-07-28 16:18:37 +03:00
91 changed files with 2773 additions and 296 deletions

View File

@ -1,5 +1,10 @@
# Changelog
### 1.51.0
- **Added**: `BaseModalViewController` implementing `PanModalPresentable` with additional functionality
- **Added**: `BaseModalWrapperViewController` for wrapping `UIViewController`s with `BaseModalViewController` functionality
### 1.50.0
- **Updated**: Fix activity indicator positioning for `StatefulButton` on iOS 15+ and disabled state touch handling

103
Makefile Normal file
View File

@ -0,0 +1,103 @@
export SRCROOT := $(shell pwd)
push_to_podspecs: TISwiftUtils.target TIFoundationUtils.target TICoreGraphicsUtils.target TIKeychainUtils.target TIUIKitCore.target TIUIElements.target TIWebView.target TIBottomSheet.target TISwiftUICore.target TITableKitUtils.target TIDeeplink.target TIDeveloperUtils.target TILogging.target TINetworking.target TIMoyaNetworking.target TINetworkingCache.target TIMapUtils.target TIAppleMapUtils.target TIGoogleMapUtils.target TIPagination.target TIAuth.target TIEcommerce.target TITextProcessing.target
$(call clean)
TISwiftUtils.target:
MODULE_NAME="TISwiftUtils" ./project-scripts/push_to_podspecs.sh
touch TISwiftUtils.target
TIFoundationUtils.target: TISwiftUtils.target
MODULE_NAME="TIFoundationUtils" ./project-scripts/push_to_podspecs.sh
touch TIFoundationUtils.target
TICoreGraphicsUtils.target:
MODULE_NAME="TICoreGraphicsUtils" ./project-scripts/push_to_podspecs.sh
touch TICoreGraphicsUtils.target
TIKeychainUtils.target: TIFoundationUtils.target
MODULE_NAME="TIKeychainUtils" ./project-scripts/push_to_podspecs.sh
touch TIKeychainUtils.target
TIUIKitCore.target: TISwiftUtils.target
MODULE_NAME="TIUIKitCore" ./project-scripts/push_to_podspecs.sh
touch TIUIKitCore.target
TIUIElements.target: TIUIKitCore.target
MODULE_NAME="TIUIElements" ./project-scripts/push_to_podspecs.sh
touch TIUIElements.target
TIWebView.target: TIUIKitCore.target
MODULE_NAME="TIWebView" ./project-scripts/push_to_podspecs.sh
touch TIWebView.target
TIBottomSheet.target: TIUIElements.target
MODULE_NAME="TIBottomSheet" ./project-scripts/push_to_podspecs.sh
touch TIBottomSheet.target
TISwiftUICore.target: TIUIKitCore.target
MODULE_NAME="TISwiftUICore" ./project-scripts/push_to_podspecs.sh
touch TISwiftUICore.target
TITableKitUtils.target: TIUIElements.target
MODULE_NAME="TITableKitUtils" ./project-scripts/push_to_podspecs.sh
touch TITableKitUtils.target
TIDeeplink.target: TIFoundationUtils.target
MODULE_NAME="TIDeeplink" ./project-scripts/push_to_podspecs.sh
touch TIDeeplink.target
TIDeveloperUtils.target: TIUIElements.target
MODULE_NAME="TIDeveloperUtils" ./project-scripts/push_to_podspecs.sh
touch TIDeveloperUtils.target
TINetworking.target: TIFoundationUtils.target
MODULE_NAME="TINetworking" ./project-scripts/push_to_podspecs.sh
touch TINetworking.target
TILogging.target:
MODULE_NAME="TILogging" ./project-scripts/push_to_podspecs.sh
touch TILogging.target
TIMoyaNetworking.target: TINetworking.target
MODULE_NAME="TIMoyaNetworking" ./project-scripts/push_to_podspecs.sh
touch TIMoyaNetworking.target
TINetworkingCache.target: TINetworking.target
MODULE_NAME="TINetworkingCache" ./project-scripts/push_to_podspecs.sh
touch TINetworkingCache.target
TIMapUtils.target: TILogging TICoreGraphicsUtils.target
MODULE_NAME="TIMapUtils" ./project-scripts/push_to_podspecs.sh
touch TIMapUtils.target
TIAppleMapUtils.target: TIMapUtils.target
MODULE_NAME="TIAppleMapUtils" ./project-scripts/push_to_podspecs.sh
touch TIAppleMapUtils.target
TIGoogleMapUtils.target: TIMapUtils.target
MODULE_NAME="TIGoogleMapUtils" ./project-scripts/push_to_podspecs.sh
touch TIGoogleMapUtils.target
TIYandexMapUtils.target: TIMapUtils.target
MODULE_NAME="TIYandexMapUtils" ./project-scripts/push_to_podspecs.sh
touch TIYandexMapUtils.target
TIPagination.target: TISwiftUtils.target
MODULE_NAME="TIPagination" ./project-scripts/push_to_podspecs.sh
touch TIPagination.target
TIAuth.target: TIUIKitCore.target TIKeychainUtils.target
MODULE_NAME="TIAuth" ./project-scripts/push_to_podspecs.sh
touch TIAuth.target
TIEcommerce.target: TINetworking.target TIUIElements.target
MODULE_NAME="TIEcommerce" ./project-scripts/push_to_podspecs.sh
touch TIEcommerce.target
TITextProcessing.target:
MODULE_NAME="TITextProcessing" ./project-scripts/push_to_podspecs.sh
touch TITextProcessing.target
clean:
rm *.target

View File

@ -5,8 +5,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/Alamofire/Alamofire.git",
"state" : {
"revision" : "f96b619bcb2383b43d898402283924b80e2c4bae",
"version" : "5.4.3"
"revision" : "bc268c28fb170f494de9e9927c371b8342979ece",
"version" : "5.7.1"
}
},
{
@ -32,8 +32,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/petropavel13/Cursors",
"state" : {
"revision" : "a1561869135e72832eff3b1e729075c56c2eebf6",
"version" : "0.5.1"
"revision" : "52f27b82cb1cbbc2b5fd09514c48b9c75e3b0300",
"version" : "0.6.0"
}
},
{
@ -50,8 +50,17 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/Moya/Moya.git",
"state" : {
"revision" : "9b906860e3c3c09032879465c471e6375829593f",
"version" : "15.0.0"
"revision" : "c263811c1f3dbf002be9bd83107f7cdc38992b26",
"version" : "15.0.3"
}
},
{
"identity" : "panmodal",
"kind" : "remoteSourceControl",
"location" : "https://git.svc.touchin.ru/TouchInstinct/PanModal",
"state" : {
"revision" : "ced7c1703f90746df0224b6e0d33c146d9ae4284",
"version" : "1.3.1"
}
},
{
@ -68,8 +77,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/ReactiveX/RxSwift.git",
"state" : {
"revision" : "b4307ba0b6425c0ba4178e138799946c3da594f8",
"version" : "6.5.0"
"revision" : "9dcaa4b333db437b0fbfaf453fad29069044a8b4",
"version" : "6.6.0"
}
},
{

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"]),
@ -54,7 +55,8 @@ let package = Package(
.package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.4.0")),
.package(url: "https://github.com/Moya/Moya.git", .upToNextMajor(from: "15.0.0")),
.package(url: "https://github.com/hyperoslo/Cache.git", .upToNextMajor(from: "6.0.0")),
.package(url: "https://github.com/antlr/antlr4", .upToNextMinor(from: "4.10.1"))
.package(url: "https://github.com/antlr/antlr4", .upToNextMinor(from: "4.10.1")),
.package(url: "https://git.svc.touchin.ru/TouchInstinct/PanModal", .upToNextMinor(from: "1.3.0"))
],
targets: [
@ -68,9 +70,16 @@ let package = Package(
plugins: [.plugin(name: "TISwiftLintPlugin")]),
.target(name: "TIWebView", dependencies: ["TIUIKitCore", "TISwiftUtils"], path: "TIWebView/Sources"),
.target(name: "TIBottomSheet",
dependencies: ["PanModal", "TIUIElements", "TIUIKitCore", "TISwiftUtils"],
path: "TIBottomSheet/Sources",
exclude: ["../TIBottomSheet.app"],
plugins: [.plugin(name: "TISwiftLintPlugin")]),
// MARK: - SwiftUI
.target(name: "TISwiftUICore", dependencies: ["TIUIKitCore", "TISwiftUtils"], path: "TISwiftUICore/Sources"),
.target(name: "TISwiftUICore",
dependencies: ["TIUIKitCore", "TISwiftUtils"],
path: "TISwiftUICore/Sources"),
// MARK: - Utils
@ -109,12 +118,12 @@ let package = Package(
plugins: [.plugin(name: "TISwiftLintPlugin")]),
.target(name: "TIMoyaNetworking",
dependencies: ["TINetworking", "TIFoundationUtils", "Moya"],
dependencies: ["TINetworking", "Moya"],
path: "TIMoyaNetworking/Sources",
plugins: [.plugin(name: "TISwiftLintPlugin")]),
.target(name: "TINetworkingCache",
dependencies: ["TIFoundationUtils", "TINetworking", "Cache"],
dependencies: ["TINetworking", "Cache"],
path: "TINetworkingCache/Sources",
plugins: [.plugin(name: "TISwiftLintPlugin")]),
@ -134,7 +143,7 @@ let package = Package(
.target(name: "OTPSwiftView", dependencies: ["TIUIElements"], path: "OTPSwiftView/Sources"),
.target(name: "TITransitions", path: "TITransitions/Sources"),
.target(name: "TIPagination", dependencies: ["Cursors", "TISwiftUtils"], path: "TIPagination/Sources"),
.target(name: "TIAuth", dependencies: ["TIFoundationUtils", "TIUIKitCore", "TIKeychainUtils"], path: "TIAuth/Sources"),
.target(name: "TIAuth", dependencies: ["TIUIKitCore", "TIKeychainUtils"], path: "TIAuth/Sources"),
.target(name: "TIEcommerce", dependencies: ["TIFoundationUtils", "TISwiftUtils", "TINetworking", "TIUIKitCore", "TIUIElements"], path: "TIEcommerce/Sources"),
.target(name: "TITextProcessing",
dependencies: [.product(name: "Antlr4", package: "antlr4")],

View File

@ -99,6 +99,7 @@ LICENSE
- [TITextProcessing](docs/titextprocessing)
* [TITextProcessing](docs/titextprocessing/titextprocessing.md)
- [TIDeeplink](docs/tideeplink/deeplinks.md)
- [TIBottomSheet](docs/tibottomsheet/tibottomsheet.md)
- [Semantic Commit Messages](docs/semantic-commit-messages.md) - commit message codestyle.
- [Snippets](docs/snippets.md) - useful commands and scripts for development.

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIAppleMapUtils'
s.version = '1.50.0'
s.version = '1.51.0'
s.summary = 'Set of helpers for map objects clustering and interacting using Apple MapKit.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIAuth'
s.version = '1.50.0'
s.version = '1.51.0'
s.summary = 'Login, registration, confirmation and other related actions'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -0,0 +1,13 @@
ENV["DEVELOPMENT_INSTALL"] = "true"
source 'https://git.svc.touchin.ru/TouchInstinct/Podspecs.git'
target 'TIBottomSheet' do
platform :ios, 11
use_frameworks!
pod 'TIUIElements', :path => '../../../../TIUIElements/TIUIElements.podspec'
ivan.smolin marked this conversation as resolved
Review

TIBottomSheet тоже надо добавить

TIBottomSheet тоже надо добавить
pod 'TIUIKitCore', :path => '../../../../TIUIKitCore/TIUIKitCore.podspec'
pod 'TISwiftUtils', :path => '../../../../TISwiftUtils/TISwiftUtils.podspec'
pod 'TIBottomSheet', :path => '../../../../TIBottomSheet/TIBottomSheet.podspec'
end

View File

@ -0,0 +1,62 @@
//
// Copyright (c) 2023 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import TIUIElements
import TIUIKitCore
import UIKit
extension BaseModalViewController {
open class BaseAppearance: UIView.BaseAppearance<UIView.DefaultWrappedLayout> {
public var presentationDetents: [ModalViewPresentationDetent]
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,
presentationDetents: [ModalViewPresentationDetent] = [.maxHeight],
dragViewState: DragView.State = .hidden,
headerViewState: ModalHeaderView.State = .hidden,
footerViewState: ModalFooterView<FooterContentView>.State = .hidden) {
self.presentationDetents = presentationDetents
self.dragViewState = dragViewState
self.headerViewState = headerViewState
self.footerViewState = footerViewState
super.init(layout: layout,
backgroundColor: backgroundColor,
border: border,
shadow: shadow)
}
}
public final class DefaultAppearance: BaseAppearance, ViewAppearance {
public static var defaultAppearance: Self {
Self()
}
}
}

View File

@ -0,0 +1,473 @@
//
// Copyright (c) 2023 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import TISwiftUtils
import TIUIElements
import TIUIKitCore
import UIKit
import PanModal
open class BaseModalViewController<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 private(set) lazy var dragViewBottomToHeaderViewTopConstraint: NSLayoutConstraint = {
dragView.bottomAnchor.constraint(equalTo: headerView.topAnchor)
}()
public private(set) lazy var dragViewBottomToContentViewTopConstraint: NSLayoutConstraint = {
dragView.bottomAnchor.constraint(equalTo: contentView.topAnchor)
}()
public private(set) lazy var dragViewConstraints: SubviewConstraints = {
let trailingConstraint = dragView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
let edgeConstraints = EdgeConstraints(leadingConstraint: dragView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
trailingConstraint: trailingConstraint,
topConstraint: dragView.topAnchor.constraint(equalTo: view.topAnchor),
bottomConstraint: dragViewBottomToHeaderViewTopConstraint)
let centerXConstraint = dragView.centerXAnchor.constraint(equalTo: view.centerXAnchor)
let centerYConstraint = dragView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
let centerConstraints = CenterConstraints(centerXConstraint: centerXConstraint,
centerYConstraint: centerYConstraint)
let sizeConstraints = SizeConstraints(widthConstraint: dragView.widthAnchor.constraint(equalToConstant: .zero),
heightConstraint: dragView.heightAnchor.constraint(equalToConstant: .zero))
return SubviewConstraints(edgeConstraints: edgeConstraints,
centerConstraints: centerConstraints,
sizeConstraints: sizeConstraints)
}()
public private(set) lazy var headerViewToSuperviewTopConstraint: NSLayoutConstraint = {
headerView.topAnchor.constraint(equalTo: view.topAnchor)
}()
public private(set) lazy var headerBottomToContentTopConstraint: NSLayoutConstraint = {
headerView.bottomAnchor.constraint(equalTo: contentView.topAnchor)
}()
public private(set) lazy var headerViewConstraints: SubviewConstraints = {
let leadingConstraint = headerView.leadingAnchor.constraint(equalTo: view.leadingAnchor)
let trailingConstraint = headerView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
let edgeConstraints = EdgeConstraints(leadingConstraint: leadingConstraint,
trailingConstraint: trailingConstraint,
topConstraint: dragViewBottomToHeaderViewTopConstraint,
bottomConstraint: headerBottomToContentTopConstraint)
let centerXConstraint = headerView.centerXAnchor.constraint(equalTo: view.centerXAnchor)
let centerYConstraint = headerView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
let centerConstraints = CenterConstraints(centerXConstraint: centerXConstraint,
centerYConstraint: centerYConstraint)
let sizeConstraints = SizeConstraints(widthConstraint: headerView.widthAnchor.constraint(equalToConstant: .zero),
heightConstraint: headerView.heightAnchor.constraint(equalToConstant: .zero))
return SubviewConstraints(edgeConstraints: edgeConstraints,
centerConstraints: centerConstraints,
sizeConstraints: sizeConstraints)
}()
public private(set) lazy var contentViewTopToSuperviewConstraint: NSLayoutConstraint = {
contentView.topAnchor.constraint(equalTo: view.topAnchor)
}()
public private(set) lazy var contentViewBottomToSuperviewConstraint: NSLayoutConstraint = {
contentView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
}()
public private(set) lazy var contentViewConstraints: SubviewConstraints = {
let leadingConstraint = contentView.leadingAnchor.constraint(equalTo: view.leadingAnchor)
let trailingConstraint = contentView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
let edgeConstraints = EdgeConstraints(leadingConstraint: leadingConstraint,
trailingConstraint: trailingConstraint,
topConstraint: headerBottomToContentTopConstraint,
bottomConstraint: contentViewBottomToSuperviewConstraint)
let centerXConstraint = contentView.centerXAnchor.constraint(equalTo: view.centerXAnchor)
let centerYConstraint = contentView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
let centerConstraints = CenterConstraints(centerXConstraint: centerXConstraint,
centerYConstraint: centerYConstraint)
let sizeConstraints = SizeConstraints(widthConstraint: contentView.widthAnchor.constraint(equalToConstant: .zero),
heightConstraint: contentView.heightAnchor.constraint(equalToConstant: .zero))
return SubviewConstraints(edgeConstraints: edgeConstraints,
ivan.smolin marked this conversation as resolved Outdated

а ты как-то учитываешь, что contentView.bounds может поменяться? типа мы обновили содержимое таблицы/коллекции?

а ты как-то учитываешь, что contentView.bounds может поменяться? типа мы обновили содержимое таблицы/коллекции?
centerConstraints: centerConstraints,
sizeConstraints: sizeConstraints)
}()
public private(set) lazy var footerViewConstraints: SubviewConstraints = {
let leadingConstraint = footerView.leadingAnchor.constraint(equalTo: view.leadingAnchor)
let trailingConstraint = footerView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
let edgeConstraints = EdgeConstraints(leadingConstraint: leadingConstraint,
trailingConstraint: trailingConstraint,
topConstraint: footerView.topAnchor.constraint(equalTo: contentView.bottomAnchor),
bottomConstraint: footerView.bottomAnchor.constraint(equalTo: view.bottomAnchor))
let centerXConstraint = footerView.centerXAnchor.constraint(equalTo: view.centerXAnchor)
let centerYConstraint = footerView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
let centerConstraints = CenterConstraints(centerXConstraint: centerXConstraint,
centerYConstraint: centerYConstraint)
let sizeConstraints = SizeConstraints(widthConstraint: footerView.widthAnchor.constraint(equalToConstant: .zero),
heightConstraint: footerView.heightAnchor.constraint(equalToConstant: .zero))
return SubviewConstraints(edgeConstraints: edgeConstraints,
centerConstraints: centerConstraints,
sizeConstraints: sizeConstraints)
}()
// MARK: - Modal View Controller Configuration
public var viewControllerAppearance: BaseAppearance = .init(backgroundColor: .white)
open var panScrollable: UIScrollView? {
contentView as? UIScrollView
}
public var panScrollableInsets: UIEdgeInsets = .zero {
didSet {
panScrollable?.contentInset = panScrollableInsets
}
}
public var dimmedView = DimmedView()
public var showDragIndicator = true
open var headerViewHeight: CGFloat {
let dragViewHeight = getHeight(of: dragView)
let headerViewHeight = getHeight(of: headerView)
let dragViewVerticalInsets = getDragViewVerticalInsets()
let headerViewVerticalInsets = getHeaderViewVerticalInsets()
return dragViewHeight + headerViewHeight + dragViewVerticalInsets + headerViewVerticalInsets
}
open var longFormHeight: PanModalHeight {
let detents = getSortedDetents()
return detents.max()?.panModalHeight(headerHeight: headerViewHeight) ?? .maxHeight
}
open var mediumFormHeight: PanModalHeight {
let detents = getSortedDetents()
if detents.count > 1 {
return detents[1].panModalHeight(headerHeight: headerViewHeight)
}
return detents.first?.panModalHeight(headerHeight: headerViewHeight) ?? .maxHeight
}
open var shortFormHeight: PanModalHeight {
let detents = getSortedDetents()
return detents.min()?.panModalHeight(headerHeight: headerViewHeight) ?? .maxHeight
}
private var keyboardDidShownObserver: NSObjectProtocol?
private var keyboardDidHiddenObserver: NSObjectProtocol?
// MARK: - Life Cycle
deinit {
let notificationCenter = NotificationCenter.default
if let keyboardDidShownObserver {
notificationCenter.removeObserver(keyboardDidShownObserver)
}
if let keyboardDidHiddenObserver {
notificationCenter.removeObserver(keyboardDidHiddenObserver)
}
}
// MARK: - BaseInitializableViewController
open override func addViews() {
super.addViews()
view.addSubviews(dragView, headerView, contentView, footerView)
}
open override func configureLayout() {
super.configureLayout()
for view in [dragView, headerView, contentView, footerView] {
view.translatesAutoresizingMaskIntoConstraints = false
}
configureDragViewLayout()
configureHeaderViewLayout()
configureContentViewLayout()
configureFooterViewLayout()
}
open override func bindViews() {
super.bindViews()
keyboardDidShownObserver = NotificationCenter.default
.addObserver(forName: UIResponder.keyboardDidShowNotification,
object: nil,
queue: .main) { [weak self] notification in
self?.configureLayoutForKeyboard(notification, isKeyboardHidden: false)
}
keyboardDidHiddenObserver = NotificationCenter.default
.addObserver(forName: UIResponder.keyboardDidHideNotification,
object: nil,
queue: .main) { [weak self] notification in
self?.configureLayoutForKeyboard(notification, isKeyboardHidden: true)
}
}
open override func configureAppearance() {
super.configureAppearance()
view.configureUIView(appearance: viewControllerAppearance)
configureDragViewAppearance()
configureHeaderViewAppearance()
configureFooterViewAppearance()
}
// MARK: - Open Methods
open func createContentView() -> ContentView {
ContentView()
}
open func createFooterView() -> ModalFooterView<FooterContentView> {
ModalFooterView<FooterContentView>()
}
open func configureLayoutForKeyboard(_ notification: Notification, isKeyboardHidden: Bool) {
guard let keyboardHeight = getKeyboardHeight(notification) else {
return
}
if case let .presented(footerViewAppearance) = viewControllerAppearance.footerViewState {
let bottomInset = footerViewAppearance.layout.insets.add(\.bottom,
to: \.bottom,
of: .vertical(bottom: keyboardHeight))
footerViewConstraints.edgeConstraints.bottomConstraint.constant = bottomInset
} else if let panScrollable {
var insets = panScrollableInsets
if isKeyboardHidden {
panScrollable.contentInset = insets
} else {
insets.bottom += keyboardHeight
panScrollable.contentInset = insets
}
}
}
// MARK: - Private Methods
private func configureDragViewLayout() {
guard case let .presented(dragViewAppearance) = viewControllerAppearance.dragViewState else {
return
}
let bottomConstraint: NSLayoutConstraint
let bottomConstant: CGFloat
if case let .presented(headerViewAppearance) = viewControllerAppearance.headerViewState {
dragViewBottomToContentViewTopConstraint.isActive = false
bottomConstraint = dragViewBottomToHeaderViewTopConstraint
bottomConstant = dragViewAppearance.layout.insets.add(\.bottom,
to: \.top,
of: headerViewAppearance.layout.insets)
} else {
dragViewBottomToHeaderViewTopConstraint.isActive = false
bottomConstraint = dragViewBottomToContentViewTopConstraint
bottomConstant = dragViewAppearance.layout.insets.bottom
}
dragViewConstraints.edgeConstraints.bottomConstraint = bottomConstraint
dragViewConstraints.update(from: dragViewAppearance.layout)
bottomConstraint.setActiveConstantOrDeactivate(constant: bottomConstant)
}
private func configureHeaderViewLayout() {
guard case let .presented(headerViewAppearance) = viewControllerAppearance.headerViewState else {
return
}
let topConstraint: NSLayoutConstraint
let topConstant: CGFloat
if case let .presented(dragViewAppearance) = viewControllerAppearance.dragViewState {
dragViewBottomToContentViewTopConstraint.isActive = false
topConstraint = dragViewBottomToHeaderViewTopConstraint
topConstant = dragViewAppearance.layout.insets.add(\.bottom,
to: \.top,
ivan.smolin marked this conversation as resolved Outdated

🤔
есть другие варианты?

🤔 есть другие варианты?
of: headerViewAppearance.layout.insets)
} else {
dragViewBottomToHeaderViewTopConstraint.isActive = false
topConstraint = headerViewToSuperviewTopConstraint
let topInset = headerViewAppearance.layout.insets.top
topConstant = topInset.isFinite ? topInset : .zero
}
headerViewConstraints.edgeConstraints.topConstraint = topConstraint
headerViewConstraints.update(from: headerViewAppearance.layout)
topConstraint.setActiveConstantOrDeactivate(constant: topConstant)
}
private func configureContentViewLayout() {
let topConstraint: NSLayoutConstraint
let topConstant: CGFloat
if case let .presented(headerViewAppearance) = viewControllerAppearance.headerViewState {
dragViewBottomToContentViewTopConstraint.isActive = false
contentViewTopToSuperviewConstraint.isActive = false
topConstraint = headerBottomToContentTopConstraint
topConstant = headerViewAppearance.layout.insets.bottom
} else if case let .presented(dragViewAppearance) = viewControllerAppearance.dragViewState {
contentViewTopToSuperviewConstraint.isActive = false
headerBottomToContentTopConstraint.isActive = false
topConstraint = dragViewBottomToContentViewTopConstraint
topConstant = dragViewAppearance.layout.insets.bottom
} else {
headerBottomToContentTopConstraint.isActive = false
dragViewBottomToContentViewTopConstraint.isActive = false
topConstraint = contentViewTopToSuperviewConstraint
topConstant = .zero
}
contentViewConstraints.edgeConstraints.topConstraint = topConstraint
let layout = UIView.DefaultWrappedLayout(insets: .horizontal(.zero)
.vertical(top: topConstant)
.replacingNan(with: .zero))
contentViewConstraints.update(from: layout)
}
private func configureFooterViewLayout() {
guard case let .presented(footerViewAppearance) = viewControllerAppearance.footerViewState else {
return
}
contentViewConstraints.edgeConstraints.bottomConstraint.isActive = false
contentViewConstraints.edgeConstraints.bottomConstraint = footerViewConstraints.edgeConstraints.topConstraint
footerViewConstraints.update(from: footerViewAppearance.layout)
}
private func configureDragViewAppearance() {
switch viewControllerAppearance.dragViewState {
case .hidden:
dragView.isHidden = true
case let .presented(appearance):
dragView.configure(appearance: appearance)
}
}
private func configureHeaderViewAppearance() {
switch viewControllerAppearance.headerViewState {
case .hidden:
headerView.isHidden = true
case let .presented(appearance):
headerView.configure(appearance: appearance)
}
}
private func configureFooterViewAppearance() {
switch viewControllerAppearance.footerViewState {
case .hidden:
footerView.isHidden = true
case let .presented(appearance):
footerView.configureBaseWrappedViewHolder(appearance: appearance)
}
}
private func getSortedDetents() -> [ModalViewPresentationDetent] {
viewControllerAppearance.presentationDetents.uniqued().sorted()
}
private func getHeight(of view: UIView) -> CGFloat {
guard !view.isHidden else {
return .zero
}
return getFittingSize(forView: view).height
}
private func getDragViewVerticalInsets() -> CGFloat {
guard case let .presented(appearance) = viewControllerAppearance.dragViewState else {
return .zero
}
return appearance.layout.insets.vertical(onNan: .zero)
}
private func getHeaderViewVerticalInsets() -> CGFloat {
guard case let .presented(appearance) = viewControllerAppearance.headerViewState else {
return .zero
}
return appearance.layout.insets.vertical(onNan: .zero)
}
private func getFittingSize(forView view: UIView) -> CGSize {
let targetSize = CGSize(width: UIScreen.main.bounds.width,
height: UIView.layoutFittingCompressedSize.height)
return view.systemLayoutSizeFitting(targetSize)
}
private func getKeyboardHeight(_ notification: Notification) -> CGFloat? {
guard let userInfo = notification.userInfo else {
return nil
}
return (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect)?.height
}
}

View File

@ -0,0 +1,54 @@
//
// Copyright (c) 2023 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import UIKit
open class DefaultModalWrapperViewController<ContentController: UIViewController>: BaseModalViewController<UIView, UIView> {
private(set) public var contentViewController: ContentController
public init(contentViewController: ContentController) {
self.contentViewController = contentViewController
super.init(nibName: nil, bundle: nil)
addChild(contentViewController)
contentViewController.didMove(toParent: self)
}
@available(*, unavailable)
required public init?(coder: NSCoder) {
contentViewController = ContentController()
super.init(coder: coder)
}
open override func createContentView() -> UIView {
contentViewController.view
}
}
public extension UIViewController {
func wrappedInBottomSheetController() -> DefaultModalWrapperViewController<UIViewController> {
DefaultModalWrapperViewController(contentViewController: self)
}
}

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,67 @@
//
// Copyright (c) 2022 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import Foundation
import PanModal
public struct ModalViewPresentationDetent: Hashable {
// MARK: - Default Values
public static var headerOnly: Self {
ivan.smolin marked this conversation as resolved Outdated
CGFLOAT_MIN? https://developer.apple.com/documentation/corefoundation/cgfloat_min
Self(height: -.greatestFiniteMagnitude)
}
public static func height(_ height: CGFloat) -> Self {
Self(height: height)
}
public static var maxHeight: Self {
ivan.smolin marked this conversation as resolved Outdated

аналогично

аналогично
Self(height: .greatestFiniteMagnitude)
}
// MARK: - Public Properties
public var height: CGFloat
// MARK: - Internal Methods
func panModalHeight(headerHeight: CGFloat = .zero) -> PanModalHeight {
if self == .headerOnly {
return .contentHeight(headerHeight)
}
if self == .maxHeight {
return .maxHeight
}
return .contentHeight(height)
}
}
// MARK: - Comparable
extension ModalViewPresentationDetent: Comparable {
public static func < (lhs: ModalViewPresentationDetent, rhs: ModalViewPresentationDetent) -> Bool {
lhs.height < rhs.height
}
}

View File

@ -0,0 +1,70 @@
//
// Copyright (c) 2023 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import TIUIElements
import TIUIKitCore
import UIKit
public final class DragView: BaseInitializableView, AppearanceConfigurable {
// MARK: - Nested Types
private enum Constants {
static var dragViewTopInset: CGFloat {
8
}
static var dragViewBorder: UIViewBorder {
UIViewBorder(cornerRadius: dragViewSize.height / 2, roundedCorners: .allCorners)
}
static var dragViewBackgroundColor: UIColor {
.lightGray
}
static var dragViewSize: CGSize {
CGSize(width: 52, height: 7)
}
}
public enum State {
case hidden
case presented(Appearance)
}
public final class Appearance: UIView.BaseWrappedAppearance<UIView.DefaultWrappedLayout>, WrappedViewAppearance {
public static var defaultAppearance: Self {
Self(layout: DefaultWrappedLayout(insets: .vertical(top: Constants.dragViewTopInset),
size: Constants.dragViewSize,
centerOffset: .centerHorizontal()),
backgroundColor: Constants.dragViewBackgroundColor,
border: Constants.dragViewBorder)
}
}
// MARK: - AppearanceConfigurable
public func configure(appearance: Appearance) {
configureUIView(appearance: appearance)
}
}

View File

@ -0,0 +1,45 @@
//
// Copyright (c) 2023 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import TIUIElements
import TIUIKitCore
import UIKit
open class ModalFooterView<ContentView: UIView>: ContainerView<ContentView> {
// MARK: - Nested Types
public enum State {
case hidden
case presented(Appearance)
}
}
public extension ModalFooterView {
final class Appearance: BaseWrappedViewHolderAppearance<DefaultWrappedAppearance, DefaultWrappedLayout>,
ivan.smolin marked this conversation as resolved Outdated

configureModalFooter(appearance:)

configureModalFooter(appearance:)

может some заиспользовать? чуть меньше кода, чуть больше гибкости в передаваемом параметре

может some заиспользовать? чуть меньше кода, чуть больше гибкости в передаваемом параметре
WrappedViewHolderAppearance {
public static var defaultAppearance: Self {
Self(layout: DefaultWrappedLayout(insets: .horizontal(.zero).vertical(bottom: .zero),
size: .fixedHeight(44)))
}
}
}

View File

@ -0,0 +1,232 @@
//
// Copyright (c) 2023 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import TIUIElements
import TIUIKitCore
import UIKit
open class ModalHeaderView: BaseInitializableView, AppearanceConfigurable {
// MARK: - Nested Types
public enum State {
case hidden
case presented(Appearance)
}
public enum ContentViewState {
case leadingButton(BaseButtonStyle)
case trailingButton(BaseButtonStyle)
case buttons(leading: BaseButtonStyle, trailing: BaseButtonStyle)
case custom(view: UIView, appearance: UIView.BaseWrappedAppearance<UIView.DefaultWrappedLayout>)
ivan.smolin marked this conversation as resolved
Review

лучше leading, trailing

лучше leading, trailing
}
// MARK: - Public properties
public let leadingButton = StatefulButton()
public let trailingButton = StatefulButton()
public private(set) lazy var leftTrailingToRightLeadingConstraint: NSLayoutConstraint = {
leadingButton.trailingAnchor.constraint(equalTo: trailingButton.leadingAnchor)
}()
public private(set) lazy var leftTrailingToSuperviewTrailing: NSLayoutConstraint = {
leadingButton.trailingAnchor.constraint(equalTo: trailingAnchor)
}()
public private(set) lazy var leadingButtonConstraints: SubviewConstraints = {
ivan.smolin marked this conversation as resolved Outdated

а почему бы не дать поддержку размеров?

а почему бы не дать поддержку размеров?
let edgeConstraints = EdgeConstraints(leadingConstraint: leadingButton.leadingAnchor.constraint(equalTo: leadingAnchor),
trailingConstraint: leftTrailingToRightLeadingConstraint,
topConstraint: leadingButton.topAnchor.constraint(equalTo: topAnchor),
bottomConstraint: leadingButton.bottomAnchor.constraint(equalTo: bottomAnchor))
let centerXConstraint = leadingButton.centerXAnchor.constraint(equalTo: centerXAnchor)
let centerYConstraint = leadingButton.centerYAnchor.constraint(equalTo: centerYAnchor)
let centerConstraints = CenterConstraints(centerXConstraint: centerXConstraint,
centerYConstraint: centerYConstraint)
let sizeConstraints = SizeConstraints(widthConstraint: leadingButton.widthAnchor.constraint(equalToConstant: .zero),
heightConstraint: leadingButton.heightAnchor.constraint(equalToConstant: .zero))
return SubviewConstraints(edgeConstraints: edgeConstraints,
centerConstraints: centerConstraints,
sizeConstraints: sizeConstraints)
}()
public private(set) lazy var rightLeadingToSuperviewLeadingConstraint: NSLayoutConstraint = {
trailingButton.leadingAnchor.constraint(equalTo: leadingAnchor)
}()
public private(set) lazy var trailingButtonConstraints: SubviewConstraints = {
let trailingConstraint = trailingButton.trailingAnchor.constraint(equalTo: trailingAnchor)
let edgeConstraints = EdgeConstraints(leadingConstraint: leftTrailingToRightLeadingConstraint,
trailingConstraint: trailingConstraint,
topConstraint: trailingButton.topAnchor.constraint(equalTo: topAnchor),
bottomConstraint: trailingButton.bottomAnchor.constraint(equalTo: bottomAnchor))
let centerXConstraint = trailingButton.centerXAnchor.constraint(equalTo: centerXAnchor)
let centerYConstraint = trailingButton.centerYAnchor.constraint(equalTo: centerYAnchor)
let centerConstraints = CenterConstraints(centerXConstraint: centerXConstraint,
centerYConstraint: centerYConstraint)
let sizeConstraints = SizeConstraints(widthConstraint: trailingButton.widthAnchor.constraint(equalToConstant: .zero),
heightConstraint: trailingButton.heightAnchor.constraint(equalToConstant: .zero))
return SubviewConstraints(edgeConstraints: edgeConstraints,
centerConstraints: centerConstraints,
sizeConstraints: sizeConstraints)
}()
public var customViewConstraints: SubviewConstraints?
// MARK: - BaseInitializableView
open override func addViews() {
super.addViews()
addSubviews(leadingButton, trailingButton)
}
open override func configureLayout() {
super.configureLayout()
for view in [leadingButton, trailingButton] {
view.translatesAutoresizingMaskIntoConstraints = false
}
}
// MARK: - AppearanceConfigurable
open func configure(appearance: Appearance) {
configureUIView(appearance: appearance)
configureContentView(state: appearance.contentViewState)
}
open func configureContentView(state: ContentViewState) {
var leadingButtonStyle: BaseButtonStyle?
var trailingButtonStyle: BaseButtonStyle?
switch state {
case let .leadingButton(style):
leadingButtonStyle = style
leadingButtonConstraints.edgeConstraints.trailingConstraint = leftTrailingToSuperviewTrailing
ivan.smolin marked this conversation as resolved Outdated

и тут

и тут
case let .trailingButton(style):
trailingButtonStyle = style
trailingButtonConstraints.edgeConstraints.leadingConstraint = rightLeadingToSuperviewLeadingConstraint
ivan.smolin marked this conversation as resolved Outdated
customViewConstraints.centerConstraints.deactivate()
```swift customViewConstraints.centerConstraints.deactivate() ```
case let .buttons(leading, trailing):
leadingButtonStyle = leading
trailingButtonStyle = trailing
case let .custom(view, appearance):
configureCustomView(view, withLayout: appearance.layout)
view.configureUIView(appearance: appearance)
}
configure(buttonStyle: leadingButtonStyle, forButton: leadingButton, constraints: leadingButtonConstraints)
configure(buttonStyle: trailingButtonStyle, forButton: trailingButton, constraints: trailingButtonConstraints)
if let leadingButtonStyle, let trailingButtonStyle {
leadingButtonConstraints.edgeConstraints.trailingConstraint = leftTrailingToRightLeadingConstraint
trailingButtonConstraints.edgeConstraints.leadingConstraint = leftTrailingToRightLeadingConstraint
let spacing = leadingButtonStyle.appearance.layout.insets.add(\.right,
to: \.left,
of: trailingButtonStyle.appearance.layout.insets)
leftTrailingToRightLeadingConstraint.setActiveConstantOrDeactivate(constant: spacing)
}
}
// MARK: - Private methods
private func configureCustomView(_ view: UIView, withLayout layout: WrappedViewLayout) {
addSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false
let edgeConstraints = EdgeConstraints(leadingConstraint: view.leadingAnchor.constraint(equalTo: leadingAnchor),
trailingConstraint: view.trailingAnchor.constraint(equalTo: trailingAnchor),
topConstraint: view.topAnchor.constraint(equalTo: topAnchor),
bottomConstraint: view.bottomAnchor.constraint(equalTo: bottomAnchor))
let centerConstraints = CenterConstraints(centerXConstraint: view.centerXAnchor.constraint(equalTo: centerXAnchor),
centerYConstraint: view.centerYAnchor.constraint(equalTo: centerYAnchor))
let sizeConstraints = SizeConstraints(widthConstraint: view.widthAnchor.constraint(equalToConstant: .zero),
heightConstraint: view.heightAnchor.constraint(equalToConstant: .zero))
let customViewConstraints = SubviewConstraints(edgeConstraints: edgeConstraints,
centerConstraints: centerConstraints,
sizeConstraints: sizeConstraints)
self.customViewConstraints = customViewConstraints
customViewConstraints.update(from: layout)
}
private func configure(buttonStyle: BaseButtonStyle?,
forButton button: StatefulButton,
constraints: SubviewConstraints?) {
guard let buttonStyle else {
button.isHidden = true
return
}
button.isHidden = false
constraints?.update(from: buttonStyle.appearance.layout)
button.apply(style: buttonStyle)
}
}
// MARK: - Appearance
public extension ModalHeaderView {
final class Appearance: BaseWrappedAppearance<DefaultWrappedLayout>, WrappedViewAppearance {
public static var defaultAppearance: Self {
Self(layout: DefaultWrappedLayout(insets: .horizontal(.zero).vertical(top: .zero),
size: .fixedHeight(44)))
}
public var contentViewState: ContentViewState
public init(layout: DefaultWrappedLayout = .defaultLayout,
backgroundColor: UIColor = .clear,
border: UIViewBorder = .init(),
shadow: UIViewShadow? = nil,
contentViewState: ContentViewState = .leadingButton(.init())) {
self.contentViewState = contentViewState
super.init(layout: layout, backgroundColor: backgroundColor, border: border, shadow: shadow)
}
}
}

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 UIKit
import PanModal
open class PassthroughDimmedView: DimmedView {
public weak var hitTestHandlerView: UIView?
public var isTransparent = false
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let hitTestHandlerView else {
return super.hitTest(point, with: event)
}
return hitTestHandlerView.hitTest(point, with: event)
}
public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
hitTestHandlerView == nil || super.point(inside: point, with: event)
}
open override func onChange(dimState: DimmedView.DimState) {
super.onChange(dimState: dimState)
if isTransparent {
alpha = .zero
}
}
}

View File

@ -0,0 +1,3 @@
**/build/
**/nef/
LICENSE

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>launcher</string>
<key>CFBundleIconFile</key>
<string>AppIcon</string>
<key>CFBundleIconName</key>
<string>AppIcon</string>
<key>CFBundleIdentifier</key>
<string>com.fortysevendeg.nef</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>
<key>LSApplicationCategoryType</key>
<string>public.app-category.developer-tools</string>
<key>LSMinimumSystemVersion</key>
<string>10.14</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2019 The nef Authors. All rights reserved.</string>
</dict>
</plist>

View File

@ -0,0 +1,26 @@
## gitignore nef files
**/build/
**/nef/
LICENSE
## User data
**/xcuserdata/
podfile.lock
**.DS_Store
## Obj-C/Swift specific
*.hmap
*.ipa
*.dSYM.zip
*.dSYM
## CocoaPods
**Pods**
## Carthage
**Carthage**
## SPM
.build
.swiftpm
swiftpm

View File

@ -0,0 +1,13 @@
ENV["DEVELOPMENT_INSTALL"] = "true"
source 'https://git.svc.touchin.ru/TouchInstinct/Podspecs.git'
target 'TIBottomSheet' do
platform :ios, 11
use_frameworks!
pod 'TIUIElements', :path => '../../../../TIUIElements/TIUIElements.podspec'
pod 'TIUIKitCore', :path => '../../../../TIUIKitCore/TIUIKitCore.podspec'
pod 'TISwiftUtils', :path => '../../../../TISwiftUtils/TISwiftUtils.podspec'
pod 'TIBottomSheet', :path => '../../../../TIBottomSheet/TIBottomSheet.podspec'
end

View File

@ -0,0 +1,108 @@
/*:
# TIBottomSheet
TIBottomSheet содержить базовую реализацию модального котроллера и немного видоизмененную библиотеку PanModal.
## Базовый контроллер
Для создания модального котроллера можно унаследоваться от `BaseModalViewController`. Данный клас принимает два generic типа: тип основного контента, тип контента футера.
*/
import TIBottomSheet
import UIKit
class EmptyViewController: BaseModalViewController<UIView, UIView> { }
/*:
## Обертка вокруг существующего контроллера
Может быть такое, что из уже существующего контроллера нужно сделать модальное окно. С этим может помочь обертка `BaseModalWrapperViewController`. Данный контроллер является наследником BaseModalViewController, что позволяет его настраивать так же, как и базовый модальный котроллер
*/
import TIUIKitCore
final class OldMassiveViewController: BaseInitializableViewController {
// some implementation
}
typealias ModalOldMassiveViewController = BaseModalWrapperViewController<OldMassiveViewController>
class PresentingViewController: BaseInitializableViewController {
// some implementation
@objc private func onButtonTapped() {
presentPanModal(ModalOldMassiveViewController())
}
}
/*:
## Контент модального контроллера
Модальный котроллер может содержать следующие элементы: `DragView`, `HeaderView`, `FooterView`. Каждый из них является опциональным и без дополнительных настроек не будет показываться.
DragView - небольшая view, за которую пользователь "держит" модальный контроллер
HeaderView - контейнер, содержащий в себе кнопки назад/закрыть или какие-то другие элементы управления
FooterView - view, располагающаяся внизу контроллера, поверх всего контента (модальный контроллер уже настроен так, чтобы при скролле в самый низ, футер не перекрывал последнюю ячейку)
Для настройки каждого у котроллера есть свойство `viewControllerAppearance`. Через него будет настраиваться весь контроллер. Однако стоит заметить, что котроллер не будет настраивать передаваимую вью, содержащую основной контент. Стандартно котроллер будет пытаться расположить контент так, чтобы он заполнил все пространство.
Вот пример настройки внешнего вида так, чтобы был видет dragView и headerView с левой кнопкой:
*/
let customViewController = BaseModalViewController<UIView, UIView>()
customViewController.viewControllerAppearance = BaseModalViewController.DefaultAppearance.make {
$0.dragViewState = .presented(.defaultAppearance)
$0.headerViewState = .presented(.make {
$0.layout.size = .fixedHeight(52)
$0.backgroundColor = .white
$0.contentViewState = .buttonLeft(.init(titles: [.normal: "Close"],
appearance: .init(stateAppearances: [
.normal: .init(backgroundColor: .blue)
])))
})
}
/*:
## "Якори" контроллера
Раньше для настройки высоты контроллера необходимо было пользоваться свойствами `longFormHeight`, `shortFormHeight`. В базовом контроллере можно лишь передать список точек на которых контроллер должен будет задержаться:
*/
let detentsViewController = BaseModalViewController<UIView, UIView>()
detentsViewController.viewControllerAppearance.presentationDetents = [.headerOnly, .height(300), .maxHeight]
/*:
- headerOnly будет сам пытаться вычеслить высоту хедера и dragView, показывая только их
- height(_) будет показывать контроллер на переданной высоте
- maxHeight - вся высота экрана (до safeArea)
В данный массив не рекомендуется передавать больше 3 значений, т.к. модальное окно все равно сможет занять только 3 положения на экране.
## DimmedView и PassthroughDimmedView
Для контроля `DimmedView` (затемняющей view) есть отдельное свойство `dimmedView`. Эти классы позволяют настраивать поведение при тапе в затемнённую область и кастомизировать затемнение под ваши нужды.
*/
let shadowViewController = BaseModalViewController<UIView, UIView>()
let dimmedView = PassthroughDimmedView()
dimmedView.hitTestHandlerView = view
dimmedView.configureUIView(appearance: .init(shadow: UIViewShadow(radius: 8,
color: .black,
opacity: 0.3)))
shadowViewController.dimmedView = dimmedView
/*:
## Контроль закрытия
`PanModalPresentable` не умеет в настройку закрытия контроллера, делая это самостоятельно через `dismiss(animated:completion:)`. Теперь можно настроить закрытие самостоятельно через свойства: `onTapToDismiss` и `onDragToDismiss`.
# Взаимодействие с PanModal
Если нет необходимости или возможности использовать `BaseModalViewController`, вы все так же можете пользоваться протоколом `PanModalRepresentable`. Вот список изменений протокола:
- Открытие/закрытие модального окна теперь можно настроить с помощью свойств `onTapToDismiss` и `onDragToDismiss`
- Можно настроить промежуточное состояние модального окна с `mediumFormHeight`
- `DimmedView` открыт для наследования и может создаваться в `dimmedView` у
> Для `BaseModalViewController` все свойства из `PanModalPresentable` все также работают, т.е. вы можете их переопределять, добавлять и изменять по необходимости.
*/

View File

@ -0,0 +1,30 @@
import UIKit
public protocol NefPlaygroundLiveViewable {}
extension UIView: NefPlaygroundLiveViewable {}
extension UIViewController: NefPlaygroundLiveViewable {}
#if NOT_IN_PLAYGROUND
public enum Nef {
public enum Playground {
public static func liveView(_ view: NefPlaygroundLiveViewable) {}
public static func needsIndefiniteExecution(_ state: Bool) {}
}
}
#else
import PlaygroundSupport
public enum Nef {
public enum Playground {
public static func liveView(_ view: NefPlaygroundLiveViewable) {
PlaygroundPage.current.liveView = (view as! PlaygroundLiveViewable)
}
public static func needsIndefiniteExecution(_ state: Bool) {
PlaygroundPage.current.needsIndefiniteExecution = state
}
}
}
#endif

View File

@ -0,0 +1,47 @@
import Foundation
import XCTest
public extension Nef {
static func run<T: XCTestCase>(testCase class: T.Type) {
startTestObserver()
T.defaultTestSuite.run()
}
static private func startTestObserver() {
_ = testObserverInstalled
}
static private var testObserverInstalled = { () -> NefTestFailObserver in
let testObserver = NefTestFailObserver()
XCTestObservationCenter.shared.addTestObserver(testObserver)
return testObserver
}()
}
// MARK: enrich the output for XCTest
fileprivate class NefTestFailObserver: NSObject, XCTestObservation {
private var numberOfFailedTests = 0
func testSuiteWillStart(_ testSuite: XCTestSuite) {
numberOfFailedTests = 0
}
func testSuiteDidFinish(_ testSuite: XCTestSuite) {
if numberOfFailedTests > 0 {
print("💢 Test Suite '\(testSuite.name)' finished with \(numberOfFailedTests) failed \(numberOfFailedTests > 1 ? "tests" : "test").")
} else {
print("🔅 Test Suite '\(testSuite.name)' finished successfully.")
}
}
func testCase(_ testCase: XCTestCase,
didFailWithDescription description: String,
inFile filePath: String?,
atLine lineNumber: Int) {
numberOfFailedTests += 1
print("Test Fail '\(testCase.name)':\(UInt(lineNumber)): \(description.description)")
}
}

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<playground version='6.0' target-platform='ios' display-mode='raw' buildActiveScheme='true'/>

View File

@ -0,0 +1,396 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 50;
objects = {
/* Begin PBXBuildFile section */
83C08983988F66570478C40D /* Pods_TIBottomSheet.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8EF3488B86B483233C2CC631 /* Pods_TIBottomSheet.framework */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
7B6955D74676A5427AC42234 /* Pods-TIBottomSheet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TIBottomSheet.debug.xcconfig"; path = "Target Support Files/Pods-TIBottomSheet/Pods-TIBottomSheet.debug.xcconfig"; sourceTree = "<group>"; };
8BACBE8322576CAD00266845 /* TIBottomSheet.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TIBottomSheet.framework; sourceTree = BUILT_PRODUCTS_DIR; };
8BACBE8622576CAD00266845 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
8EF3488B86B483233C2CC631 /* Pods_TIBottomSheet.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_TIBottomSheet.framework; sourceTree = BUILT_PRODUCTS_DIR; };
AA57D8210790AD14BCC54A7E /* Pods-TIBottomSheet.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TIBottomSheet.release.xcconfig"; path = "Target Support Files/Pods-TIBottomSheet/Pods-TIBottomSheet.release.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
8BACBE8022576CAD00266845 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
83C08983988F66570478C40D /* Pods_TIBottomSheet.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
11F06D2789C6CF40767861CF /* Frameworks */ = {
isa = PBXGroup;
children = (
8EF3488B86B483233C2CC631 /* Pods_TIBottomSheet.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
1F7782E3A7AD7291B7C09F56 /* Pods */ = {
isa = PBXGroup;
children = (
7B6955D74676A5427AC42234 /* Pods-TIBottomSheet.debug.xcconfig */,
AA57D8210790AD14BCC54A7E /* Pods-TIBottomSheet.release.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
8B39A26221D40F8700DE2643 = {
isa = PBXGroup;
children = (
8BACBE8422576CAD00266845 /* TIBottomSheet */,
8B39A26C21D40F8700DE2643 /* Products */,
1F7782E3A7AD7291B7C09F56 /* Pods */,
11F06D2789C6CF40767861CF /* Frameworks */,
);
sourceTree = "<group>";
};
8B39A26C21D40F8700DE2643 /* Products */ = {
isa = PBXGroup;
children = (
8BACBE8322576CAD00266845 /* TIBottomSheet.framework */,
);
name = Products;
sourceTree = "<group>";
};
8BACBE8422576CAD00266845 /* TIBottomSheet */ = {
isa = PBXGroup;
children = (
8BACBE8622576CAD00266845 /* Info.plist */,
);
path = TIBottomSheet;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
8BACBE7E22576CAD00266845 /* Headers */ = {
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXHeadersBuildPhase section */
/* Begin PBXNativeTarget section */
8BACBE8222576CAD00266845 /* TIBottomSheet */ = {
isa = PBXNativeTarget;
buildConfigurationList = 8BACBE8A22576CAD00266845 /* Build configuration list for PBXNativeTarget "TIBottomSheet" */;
buildPhases = (
4E98D4C60DCD00EB801E579E /* [CP] Check Pods Manifest.lock */,
8BACBE7E22576CAD00266845 /* Headers */,
8BACBE7F22576CAD00266845 /* Sources */,
8BACBE8022576CAD00266845 /* Frameworks */,
8BACBE8122576CAD00266845 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = TIBottomSheet;
productName = TIBottomSheet2;
productReference = 8BACBE8322576CAD00266845 /* TIBottomSheet.framework */;
productType = "com.apple.product-type.framework";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
8B39A26321D40F8700DE2643 /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1010;
LastUpgradeCheck = 1200;
ORGANIZATIONNAME = "47 Degrees";
TargetAttributes = {
8BACBE8222576CAD00266845 = {
CreatedOnToolsVersion = 10.1;
};
};
};
buildConfigurationList = 8B39A26621D40F8700DE2643 /* Build configuration list for PBXProject "TIBottomSheet" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 8B39A26221D40F8700DE2643;
productRefGroup = 8B39A26C21D40F8700DE2643 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
8BACBE8222576CAD00266845 /* TIBottomSheet */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
8BACBE8122576CAD00266845 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
4E98D4C60DCD00EB801E579E /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-TIBottomSheet-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
8BACBE7F22576CAD00266845 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
8B39A27721D40F8800DE2643 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_TESTING_SEARCH_PATHS = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
8B39A27821D40F8800DE2643 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTING_SEARCH_PATHS = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
};
name = Release;
};
8BACBE8822576CAD00266845 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7B6955D74676A5427AC42234 /* Pods-TIBottomSheet.debug.xcconfig */;
buildSettings = {
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Manual;
CURRENT_TIBottomSheet_VERSION = 1;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = "";
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = "$(SRCROOT)/TIBottomSheet/Info.plist";
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 12.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.47deg.ios.TIBottomSheet;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;
SWIFT_VERSION = 5;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
};
name = Debug;
};
8BACBE8922576CAD00266845 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = AA57D8210790AD14BCC54A7E /* Pods-TIBottomSheet.release.xcconfig */;
buildSettings = {
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Manual;
CURRENT_TIBottomSheet_VERSION = 1;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = "";
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = "$(SRCROOT)/TIBottomSheet/Info.plist";
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 12.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.47deg.ios.TIBottomSheet;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;
SWIFT_VERSION = 5;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
8B39A26621D40F8700DE2643 /* Build configuration list for PBXProject "TIBottomSheet" */ = {
isa = XCConfigurationList;
buildConfigurations = (
8B39A27721D40F8800DE2643 /* Debug */,
8B39A27821D40F8800DE2643 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
8BACBE8A22576CAD00266845 /* Build configuration list for PBXNativeTarget "TIBottomSheet" */ = {
isa = XCConfigurationList;
buildConfigurations = (
8BACBE8822576CAD00266845 /* Debug */,
8BACBE8922576CAD00266845 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 8B39A26321D40F8700DE2643 /* Project object */;
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:TIBottomSheet.xcodeproj">
</FileRef>
</Workspace>

View File

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1200"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "8BACBE8222576CAD00266845"
BuildableName = "TIBottomSheet.framework"
BlueprintName = "TIBottomSheet"
ReferencedContainer = "container:TIBottomSheet.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "8BACBE8222576CAD00266845"
BuildableName = "TIBottomSheet.framework"
BlueprintName = "TIBottomSheet"
ReferencedContainer = "container:TIBottomSheet.xcodeproj">
</BuildableReference>
</MacroExpansion>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "8BACBE8222576CAD00266845"
BuildableName = "TIBottomSheet.framework"
BlueprintName = "TIBottomSheet"
ReferencedContainer = "container:TIBottomSheet.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef location = "group:TIBottomSheet.playground"></FileRef>
<FileRef
location = "group:TIBottomSheet.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2019. The nef authors.</string>
</dict>
</plist>

View File

@ -0,0 +1,6 @@
#!/bin/bash
workspace="TIBottomSheet.xcworkspace"
workspacePath=$(echo "$0" | rev | cut -f2- -d '/' | rev)
open "`pwd`/$workspacePath/$workspace"

View File

@ -0,0 +1 @@
TIBottomSheet.app/Contents/MacOS/TIBottomSheet.playground

View File

@ -0,0 +1,29 @@
Pod::Spec.new do |s|
s.name = 'TIBottomSheet'
s.version = '1.51.0'
ivan.smolin marked this conversation as resolved Outdated

1.50.0

1.50.0
s.summary = 'Base models for creating bottom sheet view controllers'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { 'castlele' => 'nikita.semenov@touchin.ru',
'petropavel13' => 'ivan.smolin@touchin.ru'}
s.source = { :git => 'https://git.svc.touchin.ru/TouchInstinct/LeadKit.git', :tag => s.version.to_s }
s.ios.deployment_target = '11.0'
s.swift_versions = ['5.7']
sources = 'Sources/**/*'
if ENV["DEVELOPMENT_INSTALL"] # installing using :path =>
s.source_files = sources
s.exclude_files = s.name + '.app'
else
s.source_files = s.name + '/' + sources
s.exclude_files = s.name + '/*.app'
end
s.dependency 'TIUIElements', s.version.to_s
s.dependency 'TIUIKitCore', s.version.to_s
s.dependency 'TISwiftUtils', s.version.to_s
s.dependency 'PanModal', '~> 1.3.0'
end

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TICoreGraphicsUtils'
s.version = '1.50.0'
s.version = '1.51.0'
s.summary = 'CoreGraphics drawing helpers'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIDeeplink'
s.version = '1.50.0'
s.version = '1.51.0'
s.summary = 'Deeplink service API'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIDeveloperUtils'
s.version = '1.50.0'
s.version = '1.51.0'
s.summary = 'Universal web view API'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -60,7 +60,7 @@ open class DefaultFilterTableViewCell: ContainerTableViewCell<DefaultPickerView>
// MARK: - Open methods
open func updateAppearance(with appearance: FilterCellStateAppearance) {
contentInsets = appearance.contentInsets
wrappedContentInsets = appearance.contentInsets
wrappedView.textColor = appearance.fontColor
wrappedView.images = appearance.stateImages ?? [:]

View File

@ -56,7 +56,7 @@ open class DefaultFilterCollectionCell: ContainerCollectionViewCell<UILabel>, Co
// MARK: - Open methdos
open func updateAppearance(with appearance: FilterCellStateAppearance) {
contentInsets = appearance.contentInsets
wrappedContentInsets = appearance.contentInsets
wrappedView.textColor = appearance.fontColor
backgroundColor = appearance.backgroundColor

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIEcommerce'
s.version = '1.50.0'
s.version = '1.51.0'
s.summary = 'Cart, products, promocodes, bonuses and other related actions'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIFoundationUtils'
s.version = '1.50.0'
s.version = '1.51.0'
s.summary = 'Set of helpers for Foundation framework classes.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIGoogleMapUtils'
s.version = '1.50.0'
s.version = '1.51.0'
s.summary = 'Set of helpers for map objects clustering and interacting using Google Maps SDK.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIKeychainUtils'
s.version = '1.50.0'
s.version = '1.51.0'
s.summary = 'Set of helpers for Keychain classes.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TILogging'
s.version = '1.50.0'
s.version = '1.51.0'
s.summary = 'Logging for TI libraries.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIMapUtils'
s.version = '1.50.0'
s.version = '1.51.0'
s.summary = 'Set of helpers for map objects clustering and interacting.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIMoyaNetworking'
s.version = '1.50.0'
s.version = '1.51.0'
s.summary = 'Moya + Swagger network service.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TINetworking'
s.version = '1.50.0'
s.version = '1.51.0'
s.summary = 'Swagger-frendly networking layer helpers.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TINetworkingCache'
s.version = '1.50.0'
s.version = '1.51.0'
s.summary = 'Caching results of EndpointRequests.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIPagination'
s.version = '1.50.0'
s.version = '1.51.0'
s.summary = 'Generic pagination component.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TISwiftUICore'
s.version = '1.50.0'
s.version = '1.51.0'
s.summary = 'Core UI elements: protocols, views and helpers.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

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.50.0'
s.version = '1.51.0'
s.summary = 'Bunch of useful helpers for Swift development.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TITableKitUtils'
s.version = '1.50.0'
s.version = '1.51.0'
s.summary = 'Set of helpers for TableKit classes.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TITextProcessing'
s.version = '1.50.0'
s.version = '1.51.0'
s.summary = 'A text processing service helping to get a text mask and a placeholder from incoming regex.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -30,21 +30,21 @@ extension UIButton {
appearance.textAttributes?
.configure(button: self,
with: title(for: state),
for: state)
for: state)
if #available(iOS 15, *) {
var config = configuration ?? .plain()
configureInsets(in: &config,
contentInsets: appearance.contentLayout.contentInsets,
titleInsets: appearance.contentLayout.titleInsets,
imageInsets: appearance.contentLayout.imageInsets)
contentInsets: appearance.contentLayout.contentInsets.replacingNan(with: .zero),
titleInsets: appearance.contentLayout.titleInsets.replacingNan(with: .zero),
imageInsets: appearance.contentLayout.imageInsets.replacingNan(with: .zero))
configuration = config
} else {
contentEdgeInsets = appearance.contentLayout.contentInsets
titleEdgeInsets = appearance.contentLayout.titleInsets
imageEdgeInsets = appearance.contentLayout.imageInsets
contentEdgeInsets = appearance.contentLayout.contentInsets.replacingNan(with: .zero)
titleEdgeInsets = appearance.contentLayout.titleInsets.replacingNan(with: .zero)
imageEdgeInsets = appearance.contentLayout.imageInsets.replacingNan(with: .zero)
}
super.configureUIView(appearance: appearance)
@ -67,7 +67,7 @@ extension UIButton {
currentImage
]
.compactMap { $0 }
.first { !($0.size.width.isZero || $0.size.height.isZero) } != nil
.contains { !($0.size.width.isZero || $0.size.height.isZero) }
if hasNonEmptyImage {
let leadingContentInset: CGFloat

View File

@ -53,7 +53,7 @@ extension UIView {
public var centerOffset: UIOffset
public var insets: UIEdgeInsets
public init(insets: UIEdgeInsets = .zero,
public init(insets: UIEdgeInsets = .nan,
size: CGSize = .infinity,
centerOffset: UIOffset = .nan) {
@ -73,7 +73,7 @@ extension UIView {
open class BaseSpecedWrappedLayout: BaseWrappedLayout {
public var spacing: CGFloat
public init(insets: UIEdgeInsets = .zero,
public init(insets: UIEdgeInsets = .nan,
size: CGSize = .infinity,
centerOffset: UIOffset = .nan,
spacing: CGFloat = .zero) {
@ -98,7 +98,7 @@ extension UIView {
public var distribution: UIStackView.Distribution
public var alignment: UIStackView.Alignment
public init(insets: UIEdgeInsets = .zero,
public init(insets: UIEdgeInsets = .nan,
size: CGSize = .infinity,
centerOffset: UIOffset = .nan,
spacing: CGFloat = .zero,

View File

@ -27,15 +27,13 @@ open class ContainerSeparatorTableViewCell<View: UIView>: ContainerTableViewCell
private lazy var topSeparatorView = createTopSeparator()
private lazy var bottomSeparatorView = createBottomSeparator()
private var topViewLeftConstraint: NSLayoutConstraint?
private var topViewRightConstraint: NSLayoutConstraint?
private var topViewTopConstraint: NSLayoutConstraint?
private var topViewHeightConstraint: NSLayoutConstraint?
private lazy var topSeparatorConstraints: SubviewConstraints = {
subviewConstraints(for: topSeparatorView)
}()
private var bottomViewLeftConstraint: NSLayoutConstraint?
private var bottomViewRightConstraint: NSLayoutConstraint?
private var bottomViewBottomConstraint: NSLayoutConstraint?
private var bottomViewHeightConstraint: NSLayoutConstraint?
private lazy var bottomSeparatorConstraints: SubviewConstraints = {
subviewConstraints(for: bottomSeparatorView)
}()
open func createTopSeparator() -> UIView {
.init()
@ -93,41 +91,17 @@ open class ContainerSeparatorTableViewCell<View: UIView>: ContainerTableViewCell
open override func configureLayout() {
super.configureLayout()
if let separatorSuperview = topSeparatorView.superview {
topViewTopConstraint = topSeparatorView.topAnchor.constraint(equalTo: separatorSuperview.topAnchor)
topViewRightConstraint = separatorSuperview.rightAnchor.constraint(equalTo: topSeparatorView.rightAnchor)
topViewLeftConstraint = topSeparatorView.leftAnchor.constraint(equalTo: separatorSuperview.leftAnchor)
for view in [topSeparatorView, bottomSeparatorView] {
view.translatesAutoresizingMaskIntoConstraints = false
}
topViewHeightConstraint = topSeparatorView.heightAnchor.constraint(equalToConstant: 1)
if let separatorSuperview = topSeparatorView.superview {
bottomViewRightConstraint = separatorSuperview.rightAnchor.constraint(equalTo: bottomSeparatorView.rightAnchor)
bottomViewLeftConstraint = bottomSeparatorView.leftAnchor.constraint(equalTo: separatorSuperview.leftAnchor)
bottomViewBottomConstraint = bottomSeparatorView.bottomAnchor.constraint(equalTo: separatorSuperview.bottomAnchor)
}
bottomViewHeightConstraint = bottomSeparatorView.heightAnchor.constraint(equalToConstant: 1)
NSLayoutConstraint.activate([
topViewTopConstraint,
topViewRightConstraint,
topViewLeftConstraint,
topViewHeightConstraint,
bottomViewRightConstraint,
bottomViewLeftConstraint,
bottomViewBottomConstraint,
bottomViewHeightConstraint
].compactMap { $0 })
}
open override func configureAppearance() {
super.configureAppearance()
[topSeparatorView, bottomSeparatorView].forEach {
$0.isHidden = true
$0.backgroundColor = .black
$0.translatesAutoresizingMaskIntoConstraints = false
for view in [topSeparatorView, bottomSeparatorView] {
view.isHidden = true
view.backgroundColor = .black
}
}
@ -136,20 +110,37 @@ open class ContainerSeparatorTableViewCell<View: UIView>: ContainerTableViewCell
private func updateTopSeparator(with appearance: SeparatorAppearance) {
topSeparatorView.configureUIView(appearance: appearance)
topViewHeightConstraint?.constant = appearance.layout.size.height
topViewTopConstraint?.constant = appearance.layout.insets.top
topViewLeftConstraint?.constant = appearance.layout.insets.left
topViewRightConstraint?.constant = appearance.layout.insets.right
topSeparatorConstraints.update(from: appearance.layout)
}
private func updateBottomSeparator(with appearance: SeparatorAppearance) {
bottomSeparatorView.configureUIView(appearance: appearance)
bottomViewHeightConstraint?.constant = appearance.layout.size.height
bottomSeparatorConstraints.update(from: appearance.layout)
}
bottomViewBottomConstraint?.constant = appearance.layout.insets.bottom
bottomViewLeftConstraint?.constant = appearance.layout.insets.left
bottomViewRightConstraint?.constant = appearance.layout.insets.right
private func subviewConstraints(for seperatorView: UIView) -> SubviewConstraints {
let leadingConstraint = seperatorView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor)
let trailingConstraint = seperatorView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor)
let topConstraint = seperatorView.topAnchor.constraint(equalTo: contentView.topAnchor)
let bottomConstraint = seperatorView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
let edgeConstraints = EdgeConstraints(leadingConstraint: leadingConstraint,
trailingConstraint: trailingConstraint,
topConstraint: topConstraint,
bottomConstraint: bottomConstraint)
let centerXConstraint = seperatorView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor)
let centerYConstraint = seperatorView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor)
let centerConstraints = CenterConstraints(centerXConstraint: centerXConstraint,
centerYConstraint: centerYConstraint)
let sizeConstraints = SizeConstraints(widthConstraint: seperatorView.widthAnchor.constraint(equalToConstant: .zero),
heightConstraint: seperatorView.heightAnchor.constraint(equalToConstant: .zero))
return SubviewConstraints(edgeConstraints: edgeConstraints,
centerConstraints: centerConstraints,
sizeConstraints: sizeConstraints)
}
}

View File

@ -0,0 +1,70 @@
//
// Copyright (c) 2023 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import TIUIKitCore
import UIKit
open class BaseInitializeableScrollView: UIScrollView, InitializableViewProtocol {
public var callbacks: [ViewCallbacks] = []
override public init(frame: CGRect) {
super.init(frame: frame)
initializeView()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initializeView()
}
override open func layoutSubviews() {
super.layoutSubviews()
for callback in callbacks {
callback.onDidLayoutSubviews()
}
}
// MARK: - InitializableView
open func addViews() {
// override in subclass
}
open func configureLayout() {
// override in subclass
}
open func bindViews() {
// override in subclass
}
open func configureAppearance() {
// override in subclass
}
open func localize() {
// override in subclass
}
}

View File

@ -38,29 +38,47 @@ open class BaseListItemView<LeadingView: UIView,
}()
public private(set) lazy var leadingViewConstraints: SubviewConstraints = {
SubviewConstraints(edgeConstraints: EdgeConstraints(leadingConstraint: leadingView.leadingAnchor.constraint(equalTo: leadingAnchor),
trailingConstraint: middleViewLeadingConstraint,
topConstraint: leadingView.topAnchor.constraint(equalTo: topAnchor),
bottomConstraint: leadingView.bottomAnchor.constraint(equalTo: bottomAnchor)),
centerConstraints: CenterConstraints(centerXConstraint: leadingView.centerXAnchor.constraint(equalTo: centerXAnchor),
centerYConstraint: leadingView.centerYAnchor.constraint(equalTo: centerYAnchor)),
sizeConstraints: SizeConstraints(widthConstraint: leadingView.widthAnchor.constraint(equalToConstant: .zero),
heightConstraint: leadingView.heightAnchor.constraint(equalToConstant: .zero)))
let edgeConstraints = EdgeConstraints(leadingConstraint: leadingView.leadingAnchor.constraint(equalTo: leadingAnchor),
trailingConstraint: middleViewLeadingConstraint,
topConstraint: leadingView.topAnchor.constraint(equalTo: topAnchor),
bottomConstraint: leadingView.bottomAnchor.constraint(equalTo: bottomAnchor))
let centerXConstraint = leadingView.centerXAnchor.constraint(equalTo: centerXAnchor)
let centerYConstraint = leadingView.centerYAnchor.constraint(equalTo: centerYAnchor)
let centerConstraints = CenterConstraints(centerXConstraint: centerXConstraint,
centerYConstraint: centerYConstraint)
let sizeConstraints = SizeConstraints(widthConstraint: leadingView.widthAnchor.constraint(equalToConstant: .zero),
heightConstraint: leadingView.heightAnchor.constraint(equalToConstant: .zero))
return SubviewConstraints(edgeConstraints: edgeConstraints,
centerConstraints: centerConstraints,
sizeConstraints: sizeConstraints)
}()
public private(set) lazy var trailingViewLeadingToMiddleViewConstraint: NSLayoutConstraint = {
public private(set) lazy var trailingViewLeadingToMiddleConstraint: NSLayoutConstraint = {
trailingView.leadingAnchor.constraint(equalTo: middleView.trailingAnchor)
}()
public private(set) lazy var middleViewConstraints: SubviewConstraints = {
SubviewConstraints(edgeConstraints: EdgeConstraints(leadingConstraint: middleViewLeadingConstraint,
trailingConstraint: trailingViewLeadingToMiddleViewConstraint,
topConstraint: middleView.topAnchor.constraint(equalTo: topAnchor),
bottomConstraint: middleView.bottomAnchor.constraint(equalTo: bottomAnchor)),
centerConstraints: CenterConstraints(centerXConstraint: middleView.centerXAnchor.constraint(equalTo: centerXAnchor),
centerYConstraint: middleView.centerYAnchor.constraint(equalTo: centerYAnchor)),
sizeConstraints: SizeConstraints(widthConstraint: middleView.widthAnchor.constraint(equalToConstant: .zero),
heightConstraint: middleView.heightAnchor.constraint(equalToConstant: .zero)))
let edgeConstraints = EdgeConstraints(leadingConstraint: middleViewLeadingConstraint,
trailingConstraint: trailingViewLeadingToMiddleConstraint,
topConstraint: middleView.topAnchor.constraint(equalTo: topAnchor),
bottomConstraint: middleView.bottomAnchor.constraint(equalTo: bottomAnchor))
let centerXConstraint = middleView.centerXAnchor.constraint(equalTo: centerXAnchor)
let centerYConstraint = middleView.centerYAnchor.constraint(equalTo: centerYAnchor)
let centerConstraints = CenterConstraints(centerXConstraint: centerXConstraint,
centerYConstraint: centerYConstraint)
let sizeConstraints = SizeConstraints(widthConstraint: middleView.widthAnchor.constraint(equalToConstant: .zero),
heightConstraint: middleView.heightAnchor.constraint(equalToConstant: .zero))
return SubviewConstraints(edgeConstraints: edgeConstraints,
centerConstraints: centerConstraints,
sizeConstraints: sizeConstraints)
}()
public private(set) lazy var middleViewLeadingToSuperViewConstraint: NSLayoutConstraint = {
@ -72,14 +90,25 @@ open class BaseListItemView<LeadingView: UIView,
}()
public private(set) lazy var trailingViewConstraints: SubviewConstraints = {
SubviewConstraints(edgeConstraints: EdgeConstraints(leadingConstraint: trailingViewLeadingToMiddleViewConstraint,
trailingConstraint: trailingView.trailingAnchor.constraint(equalTo: trailingAnchor),
topConstraint: trailingView.topAnchor.constraint(equalTo: topAnchor),
bottomConstraint: trailingView.bottomAnchor.constraint(equalTo: bottomAnchor)),
centerConstraints: CenterConstraints(centerXConstraint: middleView.centerXAnchor.constraint(equalTo: centerXAnchor),
centerYConstraint: trailingView.centerYAnchor.constraint(equalTo: centerYAnchor)),
sizeConstraints: SizeConstraints(widthConstraint: trailingView.widthAnchor.constraint(equalToConstant: .zero),
heightConstraint: trailingView.heightAnchor.constraint(equalToConstant: .zero)))
let trailingConstraint = trailingView.trailingAnchor.constraint(equalTo: trailingAnchor)
let edgeConstraints = EdgeConstraints(leadingConstraint: trailingViewLeadingToMiddleConstraint,
trailingConstraint: trailingConstraint,
topConstraint: trailingView.topAnchor.constraint(equalTo: topAnchor),
bottomConstraint: trailingView.bottomAnchor.constraint(equalTo: bottomAnchor))
let centerXConstraint = middleView.centerXAnchor.constraint(equalTo: centerXAnchor)
let centerYConstraint = trailingView.centerYAnchor.constraint(equalTo: centerYAnchor)
let centerConstraints = CenterConstraints(centerXConstraint: centerXConstraint,
centerYConstraint: centerYConstraint)
let sizeConstraints = SizeConstraints(widthConstraint: trailingView.widthAnchor.constraint(equalToConstant: .zero),
heightConstraint: trailingView.heightAnchor.constraint(equalToConstant: .zero))
return SubviewConstraints(edgeConstraints: edgeConstraints,
centerConstraints: centerConstraints,
sizeConstraints: sizeConstraints)
}()
// MARK: - Public Properties
@ -105,9 +134,6 @@ open class BaseListItemView<LeadingView: UIView,
[leadingView, middleView, trailingView]
ivan.smolin marked this conversation as resolved Outdated

а зачем? в коде не вижу причины

а зачем? в коде не вижу причины

153 строка: leadingViewConstraints.edgeConstraints.leadingConstraint - опциональный констрейнт

153 строка: leadingViewConstraints.edgeConstraints.leadingConstraint - опциональный констрейнт
.forEach { $0.translatesAutoresizingMaskIntoConstraints = false }
[leadingViewConstraints, middleViewConstraints, trailingViewConstraints]
.forEach { $0.update(insets: .zero, size: .infinity, centerOffset: .nan) }
}
// MARK: - Public methods
@ -118,19 +144,19 @@ open class BaseListItemView<LeadingView: UIView,
configureUIView(appearance: appearance)
updateLeadingViewLayout(leadingViewLayout: appearance.leadingViewAppearance.layout,
middleViewLayout: appearance.middleViewAppearance.layout)
update(leadingViewLayout: appearance.leadingViewAppearance.layout,
middleViewLayout: appearance.middleViewAppearance.layout)
updateMiddleViewLayout(middleViewLayout: appearance.middleViewAppearance.layout)
update(middleViewLayout: appearance.middleViewAppearance.layout)
updateTrailingViewLayout(trailingViewLayout: appearance.trailingAppearance.layout,
middleViewLayout: appearance.middleViewAppearance.layout)
update(trailingViewLayout: appearance.trailingAppearance.layout,
middleViewLayout: appearance.middleViewAppearance.layout)
}
// MARK: - Private methdos
private func updateLeadingViewLayout(leadingViewLayout: WrappedViewLayout,
middleViewLayout: WrappedViewLayout) {
private func update(leadingViewLayout: WrappedViewLayout,
middleViewLayout: WrappedViewLayout) {
let leadingToSuperviewContraint: NSLayoutConstraint
let leadingViewLeftInset: CGFloat
@ -141,9 +167,13 @@ open class BaseListItemView<LeadingView: UIView,
leadingToSuperviewContraint = middleViewLeadingToSuperViewConstraint
leadingViewLeftInset = middleViewLayout.insets.left
} else {
middleViewLeadingConstraint.constant = leadingViewLayout.insets.right + middleViewLayout.insets.left
let middleViewLeadingConstant = leadingViewLayout.insets.add(\.left,
to: \.right,
of: middleViewLayout.insets,
onNan: .zero)
middleViewLeadingConstraint.setActiveConstantOrDeactivate(constant: middleViewLeadingConstant)
leadingViewConstraints.edgeConstraints.leadingConstraint.isActive = true
middleViewConstraints.edgeConstraints.leadingConstraint.isActive = true
middleViewLeadingToSuperViewConstraint.isActive = false
@ -154,15 +184,16 @@ open class BaseListItemView<LeadingView: UIView,
leadingViewLeftInset = leadingViewLayout.insets.left
}
leadingToSuperviewContraint.constant = leadingViewLeftInset
leadingToSuperviewContraint.constant = leadingViewLeftInset.isFinite ? leadingViewLeftInset : .zero
leadingToSuperviewContraint.isActive = true
}
private func updateMiddleViewLayout(middleViewLayout: WrappedViewLayout) {
private func update(middleViewLayout: WrappedViewLayout) {
middleViewConstraints.update(from: middleViewLayout)
}
private func updateTrailingViewLayout(trailingViewLayout: WrappedViewLayout,
middleViewLayout: WrappedViewLayout) {
private func update(trailingViewLayout: WrappedViewLayout,
middleViewLayout: WrappedViewLayout) {
let trailingToSuperviewConstraint: NSLayoutConstraint
let trailingViewRightInset: CGFloat
@ -173,10 +204,14 @@ open class BaseListItemView<LeadingView: UIView,
trailingToSuperviewConstraint = middleViewTrailingToSuperViewConstraint
trailingViewRightInset = middleViewLayout.insets.right
} else {
trailingViewLeadingToMiddleViewConstraint.constant = middleViewLayout.insets.right + trailingViewLayout.insets.left
trailingViewLeadingToMiddleViewConstraint.isActive = true
let trailingViewLeadingToMiddleConstant = middleViewLayout.insets.add(\.right,
to: \.left,
of: trailingViewLayout.insets,
onNan: .zero)
trailingViewLeadingToMiddleConstraint.setActiveConstantOrDeactivate(constant: trailingViewLeadingToMiddleConstant)
trailingViewLeadingToMiddleConstraint.isActive = true
middleViewTrailingToSuperViewConstraint.isActive = false
trailingViewConstraints.update(from: trailingViewLayout)
@ -185,6 +220,7 @@ open class BaseListItemView<LeadingView: UIView,
trailingViewRightInset = trailingViewLayout.insets.right
}
trailingToSuperviewConstraint.constant = -trailingViewRightInset
trailingToSuperviewConstraint.constant = trailingViewRightInset.isFinite ? -trailingViewRightInset : .zero
trailingToSuperviewConstraint.isActive = true
}
}

View File

@ -24,7 +24,7 @@
import TIUIKitCore
import UIKit
open class PlaceholderButtonStyle {
open class BaseButtonStyle {
public var titles: UIControl.StateTitles
public var images: UIControl.StateImages

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

@ -33,19 +33,19 @@ open class BasePlaceholderImageView<Placeholder: UIView>: UIImageView,
public private(set) lazy var wrappedView = Placeholder()
public var contentInsets: UIEdgeInsets = .zero {
public var wrappedContentInsets: UIEdgeInsets = .zero {
didSet {
update(subviewConstraints: placeholderConstraints)
}
}
public var contentSize: CGSize = .infinity {
public var wrappedContentSize: CGSize = .infinity {
didSet {
update(subviewConstraints: placeholderConstraints)
}
}
public var contentCenterOffset: UIOffset = .nan {
public var wrappedContentCenterOffset: UIOffset = .nan {
didSet {
update(subviewConstraints: placeholderConstraints)
}
@ -118,7 +118,9 @@ open class BasePlaceholderImageView<Placeholder: UIView>: UIImageView,
// MARK: - Open methods
open func configureBasePlaceholder(appearance: BaseWrappedViewHolderAppearance<UIView.DefaultWrappedAppearance, some WrappedViewLayout>) {
open func configureBasePlaceholder(appearance: BaseWrappedViewHolderAppearance<
some BaseWrappedAppearance<some ViewLayout>,
some WrappedViewLayout>) {
configureUIView(appearance: appearance)
wrappedView.configureUIView(appearance: appearance.subviewAppearance)

View File

@ -31,48 +31,78 @@ open class BasePlaceholderView<ImageView: UIView>: BaseInitializableView {
public let controlsStackView = UIStackView()
public private(set) lazy var imageViewConstraints: SubviewConstraints = {
SubviewConstraints(edgeConstraints: EdgeConstraints(leadingConstraint: imageView.leadingAnchor.constraint(equalTo: leadingAnchor),
trailingConstraint: imageView.trailingAnchor.constraint(equalTo: trailingAnchor),
topConstraint: imageView.topAnchor.constraint(equalTo: topAnchor),
bottomConstraint: imageView.bottomAnchor.constraint(equalTo: textView.topAnchor)),
centerConstraints: CenterConstraints(centerXConstraint: imageView.centerXAnchor.constraint(equalTo: centerXAnchor),
centerYConstraint: imageView.centerYAnchor.constraint(equalTo: centerYAnchor)),
sizeConstraints: SizeConstraints(widthConstraint: imageView.widthAnchor.constraint(equalToConstant: .zero),
heightConstraint: imageView.heightAnchor.constraint(equalToConstant: .zero)))
let edgeConstraints = EdgeConstraints(leadingConstraint: imageView.leadingAnchor.constraint(equalTo: leadingAnchor),
trailingConstraint: imageView.trailingAnchor.constraint(equalTo: trailingAnchor),
topConstraint: imageView.topAnchor.constraint(equalTo: topAnchor),
bottomConstraint: imageView.bottomAnchor.constraint(equalTo: textView.topAnchor))
let centerConstraints = CenterConstraints(centerXConstraint: imageView.centerXAnchor.constraint(equalTo: centerXAnchor),
centerYConstraint: imageView.centerYAnchor.constraint(equalTo: centerYAnchor))
let sizeConstraints = SizeConstraints(widthConstraint: imageView.widthAnchor.constraint(equalToConstant: .zero),
heightConstraint: imageView.heightAnchor.constraint(equalToConstant: .zero))
return SubviewConstraints(edgeConstraints: edgeConstraints,
centerConstraints: centerConstraints,
sizeConstraints: sizeConstraints)
}()
public private(set) lazy var textViewTopToSuperviewTopConstraint: NSLayoutConstraint = {
textView.topAnchor.constraint(equalTo: topAnchor)
}()
public private(set) lazy var textViewBottomToSuperviewBottomConstraint: NSLayoutConstraint = {
public private(set) lazy var textViewToSuperviewBottomConstraint: NSLayoutConstraint = {
textView.bottomAnchor.constraint(equalTo: bottomAnchor)
}()
public private(set) lazy var textViewConstraints: SubviewConstraints = {
SubviewConstraints(edgeConstraints: EdgeConstraints(leadingConstraint: textView.leadingAnchor.constraint(equalTo: leadingAnchor),
trailingConstraint: textView.trailingAnchor.constraint(equalTo: trailingAnchor),
topConstraint: textView.topAnchor.constraint(equalTo: imageView.bottomAnchor),
bottomConstraint: textView.bottomAnchor.constraint(lessThanOrEqualTo: controlsStackView.topAnchor)),
centerConstraints: CenterConstraints(centerXConstraint: textView.centerXAnchor.constraint(equalTo: centerXAnchor),
centerYConstraint: textView.centerYAnchor.constraint(equalTo: centerYAnchor)),
sizeConstraints: SizeConstraints(widthConstraint: textView.widthAnchor.constraint(equalToConstant: .zero),
heightConstraint: textView.heightAnchor.constraint(equalToConstant: .zero)))
let bottomConstraint = textView.bottomAnchor.constraint(lessThanOrEqualTo: controlsStackView.topAnchor)
let edgeConstraints = EdgeConstraints(leadingConstraint: textView.leadingAnchor.constraint(equalTo: leadingAnchor),
trailingConstraint: textView.trailingAnchor.constraint(equalTo: trailingAnchor),
topConstraint: textView.topAnchor.constraint(equalTo: imageView.bottomAnchor),
bottomConstraint: bottomConstraint)
let centerConstraints = CenterConstraints(centerXConstraint: textView.centerXAnchor.constraint(equalTo: centerXAnchor),
centerYConstraint: textView.centerYAnchor.constraint(equalTo: centerYAnchor))
let sizeConstraints = SizeConstraints(widthConstraint: textView.widthAnchor.constraint(equalToConstant: .zero),
heightConstraint: textView.heightAnchor.constraint(equalToConstant: .zero))
return SubviewConstraints(edgeConstraints: edgeConstraints,
centerConstraints: centerConstraints,
sizeConstraints: sizeConstraints)
}()
public private(set) lazy var controlsViewConstraints: SubviewConstraints = {
SubviewConstraints(edgeConstraints: EdgeConstraints(leadingConstraint: controlsStackView.leadingAnchor.constraint(equalTo: leadingAnchor),
trailingConstraint: controlsStackView.trailingAnchor.constraint(equalTo: trailingAnchor),
topConstraint: controlsStackView.topAnchor.constraint(equalTo: textView.bottomAnchor),
bottomConstraint: controlsStackView.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor)),
centerConstraints: CenterConstraints(centerXConstraint: controlsStackView.centerXAnchor.constraint(equalTo: centerXAnchor),
centerYConstraint: controlsStackView.centerYAnchor.constraint(equalTo: centerYAnchor)),
sizeConstraints: SizeConstraints(widthConstraint: controlsStackView.widthAnchor.constraint(equalToConstant: .zero),
heightConstraint: controlsStackView.heightAnchor.constraint(equalToConstant: .zero)))
let leadingConstraint = controlsStackView.leadingAnchor.constraint(equalTo: leadingAnchor)
let trailingConstraint = controlsStackView.trailingAnchor.constraint(equalTo: trailingAnchor)
let topConstraint = controlsStackView.topAnchor.constraint(equalTo: textView.bottomAnchor)
let bottomConstraint = controlsStackView.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor)
let edgeConstraints = EdgeConstraints(leadingConstraint: leadingConstraint,
trailingConstraint: trailingConstraint,
topConstraint: topConstraint,
bottomConstraint: bottomConstraint)
let centerXConstraint = controlsStackView.centerXAnchor.constraint(equalTo: centerXAnchor)
let centerYConstraint = controlsStackView.centerYAnchor.constraint(equalTo: centerYAnchor)
let centerConstraints = CenterConstraints(centerXConstraint: centerXConstraint,
centerYConstraint: centerYConstraint)
let heightConstraint = controlsStackView.heightAnchor.constraint(equalToConstant: .zero)
let sizeConstraints = SizeConstraints(widthConstraint: controlsStackView.widthAnchor.constraint(equalToConstant: .zero),
heightConstraint: heightConstraint)
return SubviewConstraints(edgeConstraints: edgeConstraints,
centerConstraints: centerConstraints,
sizeConstraints: sizeConstraints)
}()
public var keyboardDidShownObserver: NSObjectProtocol?
public var keyboardDidHiddenObserver: NSObjectProtocol?
private var keyboardDidShownObserver: NSObjectProtocol?
private var keyboardDidHiddenObserver: NSObjectProtocol?
open var isImageViewHidden: Bool {
imageView.isHidden
@ -82,15 +112,15 @@ open class BasePlaceholderView<ImageView: UIView>: BaseInitializableView {
controlsStackView.isHidden || controlsStackView.arrangedSubviews.isEmpty
}
// MARK: - Deinit
deinit {
if let keyboardDidShownObserver = keyboardDidShownObserver {
NotificationCenter.default.removeObserver(keyboardDidShownObserver)
let notificationCenter = NotificationCenter.default
if let keyboardDidShownObserver {
notificationCenter.removeObserver(keyboardDidShownObserver)
}
if let keyboardDidHiddenObserver = keyboardDidHiddenObserver {
NotificationCenter.default.removeObserver(keyboardDidHiddenObserver)
if let keyboardDidHiddenObserver {
notificationCenter.removeObserver(keyboardDidHiddenObserver)
}
}
@ -146,25 +176,19 @@ open class BasePlaceholderView<ImageView: UIView>: BaseInitializableView {
// MARK: - Open methods
open func applyBaseStyle(style: BasePlaceholderStyle<some BaseAppearance<some WrappedViewAppearance> & ViewAppearance>) {
open func applyBase(style: BasePlaceholderStyle<some BaseAppearance<some WrappedViewAppearance> & ViewAppearance>) {
textView.configure(with: style.titleSubtitle)
controlsStackView.axis = style.controlsViewAxis
style.buttonsStyles.forEach {
let button = StatefulButton(type: .custom)
button.set(titles: $0.titles)
button.set(images: $0.images)
button.configureStatefulButton(appearance: $0.appearance)
if let action = $0.action {
button.addTarget(action.target, action: action.action, for: action.event)
}
button.apply(style: $0)
controlsStackView.addArrangedSubview(button)
}
configureAppearance(appearance: style.appearance)
configureBase(appearance: style.appearance)
}
open func addAction(_ action: UIButton.Action,
@ -175,10 +199,10 @@ open class BasePlaceholderView<ImageView: UIView>: BaseInitializableView {
button?.addTarget(action.target, action: action.action, for: action.event)
}
open func configureAppearance(appearance: BaseAppearance<some WrappedViewAppearance>) {
configureImageViewLayout(layout: appearance.imageViewAppearance.layout)
configureTextViewLayout(layout: appearance.textViewAppearance.layout)
configureControlsViewLayout(layout: appearance.controlsViewAppearance.layout)
open func configureBase(appearance: BaseAppearance<some WrappedViewAppearance>) {
configureImageView(layout: appearance.imageViewAppearance.layout)
configureTextView(layout: appearance.textViewAppearance.layout)
configureControlsView(layout: appearance.controlsViewAppearance.layout)
configureUIView(appearance: appearance)
textView.configure(appearance: appearance.textViewAppearance)
@ -195,7 +219,7 @@ open class BasePlaceholderView<ImageView: UIView>: BaseInitializableView {
// MARK: - Private methods
private func configureImageViewLayout(layout: WrappedViewLayout) {
private func configureImageView(layout: WrappedViewLayout) {
guard !isImageViewHidden else {
imageViewConstraints.deactivate()
return
@ -204,17 +228,17 @@ open class BasePlaceholderView<ImageView: UIView>: BaseInitializableView {
imageViewConstraints.update(from: layout)
}
private func configureTextViewLayout(layout: WrappedViewLayout) {
private func configureTextView(layout: WrappedViewLayout) {
textViewConstraints.edgeConstraints.topConstraint.isActive = !isImageViewHidden
textViewTopToSuperviewTopConstraint.isActive = isImageViewHidden
textViewConstraints.edgeConstraints.bottomConstraint.isActive = !isControlsViewHidden
textViewBottomToSuperviewBottomConstraint.isActive = isControlsViewHidden
textViewToSuperviewBottomConstraint.isActive = isControlsViewHidden
textViewConstraints.update(from: layout)
}
private func configureControlsViewLayout(layout: SpacedWrappedViewLayout) {
private func configureControlsView(layout: SpacedWrappedViewLayout) {
guard !isControlsViewHidden else {
controlsViewConstraints.deactivate()
return

View File

@ -32,7 +32,7 @@ public final class DefaultPlaceholderView: BasePlaceholderView<UIImageView>, App
public func apply(style: DefaultPlaceholderStyle) {
imageView.image = style.image
super.applyBaseStyle(style: style)
super.applyBase(style: style)
guard let image = imageView.image else {
return
@ -44,14 +44,14 @@ public final class DefaultPlaceholderView: BasePlaceholderView<UIImageView>, App
// MARK: - AppearanceConfigurable
public func configure(appearance: Appearance) {
configureAppearance(appearance: appearance)
configureBase(appearance: appearance)
}
}
// MARK: - Default appearance model
extension DefaultPlaceholderView {
public final class Appearance: BaseAppearance<UIView.DefaultWrappedAppearance>, ViewAppearance {
public extension DefaultPlaceholderView {
final class Appearance: BaseAppearance<UIView.DefaultWrappedAppearance>, ViewAppearance {
public static var defaultAppearance: Self {
.init()
}

View File

@ -0,0 +1,33 @@
//
// Copyright (c) 2023 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
public extension StatefulButton {
func apply(style: BaseButtonStyle) {
set(titles: style.titles)
set(images: style.images)
configureStatefulButton(appearance: style.appearance)
if let action = style.action {
addTarget(action.target, action: action.action, for: action.event)
}
}
}

View File

@ -23,8 +23,8 @@
import UIKit
public struct CenterConstraints: ConstraintsSet {
public let centerXConstraint: NSLayoutConstraint
public let centerYConstraint: NSLayoutConstraint
public var centerXConstraint: NSLayoutConstraint
public var centerYConstraint: NSLayoutConstraint
// MARK: - ConstraintsSet

View File

@ -23,10 +23,10 @@
import UIKit
public struct EdgeConstraints: ConstraintsSet {
public let leadingConstraint: NSLayoutConstraint
public let trailingConstraint: NSLayoutConstraint
public let topConstraint: NSLayoutConstraint
public let bottomConstraint: NSLayoutConstraint
public var leadingConstraint: NSLayoutConstraint
public var trailingConstraint: NSLayoutConstraint
public var topConstraint: NSLayoutConstraint
public var bottomConstraint: NSLayoutConstraint
// MARK: - ConstraintsSet

View File

@ -22,7 +22,7 @@
import UIKit.NSLayoutConstraint
extension NSLayoutConstraint {
public extension NSLayoutConstraint {
func setActiveConstantOrDeactivate(constant: CGFloat) {
if constant.isFinite {
self.constant = constant

View File

@ -23,8 +23,8 @@
import UIKit
public struct SizeConstraints: ConstraintsSet {
public let widthConstraint: NSLayoutConstraint
public let heightConstraint: NSLayoutConstraint
public var widthConstraint: NSLayoutConstraint
public var heightConstraint: NSLayoutConstraint
// MARK: - ConstraintsSet

View File

@ -35,8 +35,8 @@ public struct SubviewConstraints: ConstraintsSet {
}
public init(edgeConstraints: EdgeConstraints,
centerConstraints: CenterConstraints,
sizeConstraints: SizeConstraints) {
centerConstraints: CenterConstraints,
sizeConstraints: SizeConstraints) {
self.edgeConstraints = edgeConstraints
self.centerConstraints = centerConstraints
@ -47,21 +47,16 @@ public struct SubviewConstraints: ConstraintsSet {
centerConstraints.update(offset: centerOffset)
sizeConstraints.update(from: size)
edgeConstraints.update(from: insets)
for verticalConstraint in edgeConstraints.vertical {
verticalConstraint.isActive = !centerOffset.vertical.isFinite
}
for horizontalConstraint in edgeConstraints.horizontal {
horizontalConstraint.isActive = !centerOffset.horizontal.isFinite
}
}
// MARK: - WrappedViewLayout shortcut
func update(from layout: some WrappedViewLayout) {
@discardableResult
public func update(from layout: some WrappedViewLayout) -> Self {
update(insets: layout.insets,
size: layout.size,
centerOffset: layout.centerOffset)
return self
}
}

View File

@ -31,7 +31,7 @@ open class CollectionTableViewCell<CollectionView: UICollectionView>: ContainerT
if #available(iOS 16, *) {
contentSizeObservation = wrappedView.observe(\.contentSize,
options: [.new]) { collectionView, change in
options: [.new]) { collectionView, change in
if let contentSize = change.newValue, !contentSize.equalTo(collectionView.bounds.size) {
self.invalidateIntrinsicContentSize()
@ -52,12 +52,12 @@ open class CollectionTableViewCell<CollectionView: UICollectionView>: ContainerT
}
let cachedCollectionFrame = wrappedView.frame
wrappedView.frame.size.width = targetSize.width - contentInsets.left - contentInsets.right
wrappedView.frame.size.width = max(targetSize.width - wrappedContentInsets.horizontal(onNan: .zero), .zero)
let collectionContentHeight = wrappedView.collectionViewLayout.collectionViewContentSize.height
wrappedView.frame = cachedCollectionFrame
return CGSize(width: targetSize.width,
height: collectionContentHeight + contentInsets.top + contentInsets.bottom)
height: collectionContentHeight + wrappedContentInsets.vertical(onNan: .zero))
}
// MARK: - Open methods

View File

@ -26,24 +26,24 @@ import TIUIKitCore
open class ContainerCollectionViewCell<View: UIView>: UICollectionViewCell, InitializableViewProtocol, WrappedViewHolder {
public var callbacks: [ViewCallbacks] = []
// MARK: - WrappedViewHolder
public private(set) lazy var wrappedView = createView()
public var contentInsets: UIEdgeInsets = .zero {
public var wrappedContentInsets: UIEdgeInsets = .zero {
didSet {
update(subviewConstraints: subviewContraints)
}
}
public var contentSize: CGSize = .infinity {
public var wrappedContentSize: CGSize = .infinity {
didSet {
update(subviewConstraints: subviewContraints)
}
}
public var contentCenterOffset: UIOffset = .nan {
public var wrappedContentCenterOffset: UIOffset = .nan {
didSet {
update(subviewConstraints: subviewContraints)
}
@ -98,6 +98,6 @@ open class ContainerCollectionViewCell<View: UIView>: UICollectionViewCell, Init
}
open func createView() -> View {
return View()
View()
}
}

View File

@ -0,0 +1,73 @@
//
// Copyright (c) 2023 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import TIUIKitCore
import UIKit
open class ContainerScrollView<ContentView: UIView>: BaseInitializeableScrollView, WrappedViewHolder {
// MARK: - WrappedViewHolder
public private(set) lazy var wrappedView = createView()
public var wrappedContentInsets: UIEdgeInsets = .zero {
didSet {
update(subviewConstraints: subviewContraints)
}
}
public var wrappedContentSize: CGSize = .infinity {
didSet {
update(subviewConstraints: subviewContraints)
}
}
public var wrappedContentCenterOffset: UIOffset = .nan {
didSet {
update(subviewConstraints: subviewContraints)
}
}
private lazy var subviewContraints: SubviewConstraints = {
configureWrappedViewLayout()
}()
open func createView() -> ContentView {
ContentView()
}
// MARK: - InitializableViewProtocol
open override func addViews() {
super.addViews()
addSubview(wrappedView)
}
}
extension ContainerScrollView: AppearanceConfigurable where View: AppearanceConfigurable,
View.Appearance: WrappedViewAppearance {
vladimir.makarov marked this conversation as resolved Outdated

Тут поехало немного

Тут поехало немного

Автовыравнивание, я бы не фокусировался на этих мелочах

Автовыравнивание, я бы не фокусировался на этих мелочах
public func configure(appearance: DefaultWrappedViewHolderAppearance<View.Appearance, NoLayout>) {
configureWrappedViewHolder(appearance: appearance)
}
}

View File

@ -24,24 +24,24 @@ import UIKit
import TIUIKitCore
open class ContainerTableViewCell<View: UIView>: BaseInitializableCell, WrappedViewHolder {
// MARK: - WrappedViewHolder
public private(set) lazy var wrappedView = createView()
public var contentInsets: UIEdgeInsets = .zero {
public var wrappedContentInsets: UIEdgeInsets = .zero {
didSet {
update(subviewConstraints: subviewContraints)
}
}
public var contentSize: CGSize = .infinity {
public var wrappedContentSize: CGSize = .infinity {
didSet {
update(subviewConstraints: subviewContraints)
}
}
public var contentCenterOffset: UIOffset = .nan {
public var wrappedContentCenterOffset: UIOffset = .nan {
didSet {
update(subviewConstraints: subviewContraints)
}
@ -66,14 +66,7 @@ open class ContainerTableViewCell<View: UIView>: BaseInitializableCell, WrappedV
}
open func createView() -> View {
return View()
}
// MARK: - Open methods
public func configureContainerTableViewCell(appearance: BaseWrappedViewHolderAppearance<some WrappedViewAppearance, some ViewLayout>) {
updateContentLayout(from: appearance.subviewAppearance.layout)
configureUIView(appearance: appearance)
View()
}
}
@ -82,8 +75,7 @@ open class ContainerTableViewCell<View: UIView>: BaseInitializableCell, WrappedV
extension ContainerTableViewCell: AppearanceConfigurable where View: AppearanceConfigurable,
View.Appearance: WrappedViewAppearance {
public func configure(appearance: DefaultWrappedViewHolderAppearance<View.Appearance, UIView.NoLayout>) {
configureContainerTableViewCell(appearance: appearance)
wrappedView.configure(appearance: appearance.subviewAppearance)
public func configure(appearance: DefaultWrappedViewHolderAppearance<View.Appearance, NoLayout>) {
configureWrappedViewHolder(appearance: appearance)
}
}

View File

@ -23,23 +23,23 @@
import TIUIKitCore
import UIKit
public final class ContainerView<View: UIView>: BaseInitializableView, WrappedViewHolder {
open class ContainerView<View: UIView>: BaseInitializableView, WrappedViewHolder {
public private(set) lazy var wrappedView = View()
public var contentInsets: UIEdgeInsets = .zero {
public var wrappedContentInsets: UIEdgeInsets = .zero {
didSet {
update(subviewConstraints: subviewContraints)
}
}
public var contentSize: CGSize = .infinity {
public var wrappedContentSize: CGSize = .infinity {
didSet {
update(subviewConstraints: subviewContraints)
}
}
public var contentCenterOffset: UIOffset = .nan {
public var wrappedContentCenterOffset: UIOffset = .nan {
didSet {
update(subviewConstraints: subviewContraints)
}
@ -80,8 +80,6 @@ extension ContainerView: AppearanceConfigurable where View: AppearanceConfigurab
public typealias Appearance = UIView.DefaultWrappedViewHolderAppearance<View.Appearance, UIView.DefaultWrappedLayout>
public func configure(appearance: Appearance) {
wrappedView.configure(appearance: appearance.subviewAppearance)
configureUIView(appearance: appearance)
updateContentLayout(from: appearance.subviewAppearance.layout)
configureWrappedViewHolder(appearance: appearance)
}
}

View File

@ -23,26 +23,28 @@
import UIKit
import TIUIKitCore
open class ReusableCollectionContainerView<View: UIView>: UICollectionReusableView, InitializableViewProtocol, WrappedViewHolder {
open class ReusableCollectionContainerView<View: UIView>: UICollectionReusableView,
InitializableViewProtocol,
WrappedViewHolder {
public var callbacks: [ViewCallbacks] = []
// MARK: - WrappedViewHolder
public private(set) lazy var wrappedView = createView()
public var contentInsets: UIEdgeInsets = .zero {
public var wrappedContentInsets: UIEdgeInsets = .zero {
didSet {
update(subviewConstraints: subviewContraints)
}
}
public var contentSize: CGSize = .infinity {
public var wrappedContentSize: CGSize = .infinity {
didSet {
update(subviewConstraints: subviewContraints)
}
}
public var contentCenterOffset: UIOffset = .nan {
public var wrappedContentCenterOffset: UIOffset = .nan {
didSet {
update(subviewConstraints: subviewContraints)
}
@ -97,6 +99,6 @@ open class ReusableCollectionContainerView<View: UIView>: UICollectionReusableVi
}
open func createView() -> View {
return View()
View()
}
}

View File

@ -0,0 +1,42 @@
//
// Copyright (c) 2023 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import TIUIKitCore
import UIKit.UIView
public extension WrappedViewHolder {
func configureBaseWrappedViewHolder(appearance: some UIView.BaseWrappedViewHolderAppearance<
some WrappedViewAppearance,
some ViewLayout>) {
updateContentLayout(from: appearance.subviewAppearance.layout)
contentView.configureUIView(appearance: appearance)
}
}
public extension WrappedViewHolder where View: AppearanceConfigurable, View.Appearance: WrappedViewAppearance {
func configureWrappedViewHolder(appearance: some UIView.BaseWrappedViewHolderAppearance<
View.Appearance,
some ViewLayout>) {
configureBaseWrappedViewHolder(appearance: appearance)
wrappedView.configure(appearance: appearance.subviewAppearance)
}
}

View File

@ -28,9 +28,10 @@ public protocol WrappedViewHolder: AnyObject {
var wrappedView: View { get }
var contentView: UIView { get }
var contentInsets: UIEdgeInsets { get set }
var contentSize: CGSize { get set }
var contentCenterOffset: UIOffset { get set }
var wrappedContentInsets: UIEdgeInsets { get set }
var wrappedContentSize: CGSize { get set }
var wrappedContentCenterOffset: UIOffset { get set }
}
public extension WrappedViewHolder {
@ -65,17 +66,17 @@ public extension WrappedViewHolder {
// MARK: - SubviewConstraints shortcut
func update(subviewConstraints: SubviewConstraints) {
subviewConstraints.update(insets: contentInsets,
size: contentSize,
centerOffset: contentCenterOffset)
subviewConstraints.update(insets: wrappedContentInsets,
size: wrappedContentSize,
centerOffset: wrappedContentCenterOffset)
}
// MARK: - WrappedViewLayout shortcut
func updateContentLayout(from layout: some WrappedViewLayout) {
contentInsets = layout.insets
contentSize = layout.size
contentCenterOffset = layout.centerOffset
wrappedContentInsets = layout.insets
wrappedContentSize = layout.size
wrappedContentCenterOffset = layout.centerOffset
}
}

View File

@ -57,10 +57,10 @@ let customStyle = DefaultPlaceholderStyle(
}, buttonsStyles: [
.init(titles: [.normal: "Reload"],
appearance: .init(stateAppearances: [
.normal: UIButton.DefaultAppearance(textAttributes: .init(font: .systemFont(ofSize: 20),
color: .black,
alignment: .natural,
isMultiline: false))
.normal: UIButton.DefaultStateAppearance(textAttributes: .init(font: .systemFont(ofSize: 20),
color: .black,
alignment: .natural,
isMultiline: false))
]))
])

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIUIElements'
s.version = '1.50.0'
s.version = '1.51.0'
s.summary = 'Bunch of useful protocols and views.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -26,28 +26,40 @@ public extension UIEdgeInsets {
// MARK: - Factory methods
static var nan: Self {
.edges(.nan)
}
static func edges(_ insets: CGFloat) -> UIEdgeInsets {
.init(top: insets, left: insets, bottom: insets, right: insets)
}
static func horizontal(_ insets: CGFloat) -> UIEdgeInsets {
.init(top: .zero, left: insets, bottom: .zero, right: insets)
.init(top: .nan, left: insets, bottom: .nan, right: insets)
}
static func vertical(_ insets: CGFloat) -> UIEdgeInsets {
.init(top: insets, left: .zero, bottom: insets, right: .zero)
.init(top: insets, left: .nan, bottom: insets, right: .nan)
}
static func horizontal(left: CGFloat = .zero, right: CGFloat = .zero) -> UIEdgeInsets {
.init(top: .zero, left: left, bottom: .zero, right: right)
static func horizontal(left: CGFloat = .nan, right: CGFloat = .nan) -> UIEdgeInsets {
.init(top: .nan, left: left, bottom: .nan, right: right)
}
static func vertical(top: CGFloat = .zero, bottom: CGFloat = .zero) -> UIEdgeInsets {
.init(top: top, left: .zero, bottom: bottom, right: .zero)
static func vertical(top: CGFloat = .nan, bottom: CGFloat = .nan) -> UIEdgeInsets {
.init(top: top, left: .nan, bottom: bottom, right: .nan)
}
// MARK: - Instance methods
func vertical(onNan defaultValue: CGFloat = .nan) -> CGFloat {
add(\.top, to: \.bottom, of: self, onNan: defaultValue)
}
func horizontal(onNan defaultValue: CGFloat = .nan) -> CGFloat {
add(\.left, to: \.right, of: self, onNan: defaultValue)
}
func horizontal(_ insets: CGFloat) -> UIEdgeInsets {
.init(top: top, left: insets, bottom: bottom, right: insets)
}
@ -56,11 +68,38 @@ public extension UIEdgeInsets {
.init(top: insets, left: left, bottom: insets, right: right)
}
func horizontal(left: CGFloat = .zero, right: CGFloat = .zero) -> UIEdgeInsets {
func horizontal(left: CGFloat = .nan, right: CGFloat = .nan) -> UIEdgeInsets {
.init(top: top, left: left, bottom: bottom, right: right)
}
func vertical(top: CGFloat = .zero, bottom: CGFloat = .zero) -> UIEdgeInsets {
func vertical(top: CGFloat = .nan, bottom: CGFloat = .nan) -> UIEdgeInsets {
.init(top: top, left: left, bottom: bottom, right: right)
}
func replacingNan(with defaultValue: CGFloat) -> Self {
.init(top: top.isFinite ? top : defaultValue,
left: left.isFinite ? left : defaultValue,
bottom: bottom.isFinite ? bottom : defaultValue,
right: right.isFinite ? right : defaultValue)
}
func add(_ lhsKeyPath: KeyPath<Self, CGFloat>,
to rhsKeyPath: KeyPath<Self, CGFloat>,
of rhs: Self,
onNan defaultValue: CGFloat = .nan) -> CGFloat {
let lhsValue = self[keyPath: lhsKeyPath]
let rhsValue = rhs[keyPath: rhsKeyPath]
switch (lhsValue.isFinite, rhsValue.isFinite) {
case (true, true):
return lhsValue + rhsValue
case (true, false):
return lhsValue
case (false, true):
return rhsValue
case (false, false):
return defaultValue
}
}
}

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIUIKitCore'
s.version = '1.50.0'
s.version = '1.51.0'
s.summary = 'Core UI elements: protocols, views and helpers.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIWebView'
s.version = '1.50.0'
s.version = '1.51.0'
s.summary = 'Universal web view API'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIYandexMapUtils'
s.version = '1.50.0'
s.version = '1.51.0'
s.summary = 'Set of helpers for map objects clustering and interacting using Yandex Maps SDK.'
s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -0,0 +1,108 @@
# TIBottomSheet
TIBottomSheet содержить базовую реализацию модального котроллера и немного видоизмененную библиотеку PanModal.
## Базовый контроллер
Для создания модального котроллера можно унаследоваться от `BaseModalViewController`. Данный клас принимает два generic типа: тип основного контента, тип контента футера.
```swift
import TIBottomSheet
import UIKit
class EmptyViewController: BaseModalViewController<UIView, UIView> { }
```
## Обертка вокруг существующего контроллера
Может быть такое, что из уже существующего контроллера нужно сделать модальное окно. С этим может помочь обертка `BaseModalWrapperViewController`. Данный контроллер является наследником BaseModalViewController, что позволяет его настраивать так же, как и базовый модальный котроллер
```swift
import TIUIKitCore
final class OldMassiveViewController: BaseInitializableViewController {
// some implementation
}
typealias ModalOldMassiveViewController = BaseModalWrapperViewController<OldMassiveViewController>
class PresentingViewController: BaseInitializableViewController {
// some implementation
@objc private func onButtonTapped() {
presentPanModal(ModalOldMassiveViewController())
}
}
```
## Контент модального контроллера
Модальный котроллер может содержать следующие элементы: `DragView`, `HeaderView`, `FooterView`. Каждый из них является опциональным и без дополнительных настроек не будет показываться.
DragView - небольшая view, за которую пользователь "держит" модальный контроллер
HeaderView - контейнер, содержащий в себе кнопки назад/закрыть или какие-то другие элементы управления
FooterView - view, располагающаяся внизу контроллера, поверх всего контента (модальный контроллер уже настроен так, чтобы при скролле в самый низ, футер не перекрывал последнюю ячейку)
Для настройки каждого у котроллера есть свойство `viewControllerAppearance`. Через него будет настраиваться весь контроллер. Однако стоит заметить, что котроллер не будет настраивать передаваимую вью, содержащую основной контент. Стандартно котроллер будет пытаться расположить контент так, чтобы он заполнил все пространство.
Вот пример настройки внешнего вида так, чтобы был видет dragView и headerView с левой кнопкой:
```swift
let customViewController = BaseModalViewController<UIView, UIView>()
customViewController.viewControllerAppearance = BaseModalViewController.DefaultAppearance.make {
$0.dragViewState = .presented(.defaultAppearance)
$0.headerViewState = .presented(.make {
$0.layout.size = .fixedHeight(52)
$0.backgroundColor = .white
$0.contentViewState = .buttonLeft(.init(titles: [.normal: "Close"],
appearance: .init(stateAppearances: [
.normal: .init(backgroundColor: .blue)
])))
})
}
```
## "Якори" контроллера
Раньше для настройки высоты контроллера необходимо было пользоваться свойствами `longFormHeight`, `shortFormHeight`. В базовом контроллере можно лишь передать список точек на которых контроллер должен будет задержаться:
```swift
let detentsViewController = BaseModalViewController<UIView, UIView>()
detentsViewController.viewControllerAppearance.presentationDetents = [.headerOnly, .height(300), .maxHeight]
```
- headerOnly будет сам пытаться вычеслить высоту хедера и dragView, показывая только их
- height(_) будет показывать контроллер на переданной высоте
- maxHeight - вся высота экрана (до safeArea)
В данный массив не рекомендуется передавать больше 3 значений, т.к. модальное окно все равно сможет занять только 3 положения на экране.
## DimmedView и PassthroughDimmedView
Для контроля `DimmedView` (затемняющей view) есть отдельное свойство `dimmedView`. Эти классы позволяют настраивать поведение при тапе в затемнённую область и кастомизировать затемнение под ваши нужды.
```swift
let shadowViewController = BaseModalViewController<UIView, UIView>()
let dimmedView = PassthroughDimmedView()
dimmedView.hitTestHandlerView = view
dimmedView.configureUIView(appearance: .init(shadow: UIViewShadow(radius: 8,
color: .black,
opacity: 0.3)))
shadowViewController.dimmedView = dimmedView
```
## Контроль закрытия
`PanModalPresentable` не умеет в настройку закрытия контроллера, делая это самостоятельно через `dismiss(animated:completion:)`. Теперь можно настроить закрытие самостоятельно через свойства: `onTapToDismiss` и `onDragToDismiss`.
# Взаимодействие с PanModal
Если нет необходимости или возможности использовать `BaseModalViewController`, вы все так же можете пользоваться протоколом `PanModalRepresentable`. Вот список изменений протокола:
- Открытие/закрытие модального окна теперь можно настроить с помощью свойств `onTapToDismiss` и `onDragToDismiss`
- Можно настроить промежуточное состояние модального окна с `mediumFormHeight`
- `DimmedView` открыт для наследования и может создаваться в `dimmedView` у
> Для `BaseModalViewController` все свойства из `PanModalPresentable` все также работают, т.е. вы можете их переопределять, добавлять и изменять по необходимости.

View File

@ -60,10 +60,10 @@ let customStyle = DefaultPlaceholderStyle(
}, buttonsStyles: [
.init(titles: [.normal: "Reload"],
appearance: .init(stateAppearances: [
.normal: UIButton.DefaultAppearance(textAttributes: .init(font: .systemFont(ofSize: 20),
color: .black,
alignment: .natural,
isMultiline: false))
.normal: UIButton.DefaultStateAppearance(textAttributes: .init(font: .systemFont(ofSize: 20),
color: .black,
alignment: .natural,
isMultiline: false))
]))
])

View File

@ -20,4 +20,5 @@ TIEcommerce
TIWebView
TIDeveloperUtils
TIDeeplink
TITextProcessing
TITextProcessing
TIBottomSheet

View File

@ -9,14 +9,23 @@
# Required environment variables:
# SRCROOT - path to project folder.
#
# Optional environment variables:
# MODULE_NAME - single module to push
#
# Examples of usage:
# SRCROOT=`pwd` ./project-scripts/push_to_podspecs.sh
#
for module_name in $(cat ${SRCROOT}/project-scripts/ordered_modules_list.txt); do
bundle exec pod repo push https://git.svc.touchin.ru/TouchInstinct/Podspecs ${SRCROOT}/${module_name}/${module_name}.podspec "$@" --allow-warnings
if [ $? -ne 0 ]; then
exit $?
fi
done
GIT_REPO_PATH="git.svc.touchin.ru/TouchInstinct/Podspecs"
vladimir.makarov marked this conversation as resolved Outdated

А это так и должно быть, да?

А это так и должно быть, да?

утёк тестовый код)

утёк тестовый код)
if [ -z "${MODULE_NAME}" ]; then
for module_name in $(cat ${SRCROOT}/project-scripts/ordered_modules_list.txt); do
bundle exec pod repo push "git@${GIT_REPO_PATH}.git" ${SRCROOT}/${module_name}/${module_name}.podspec "$@" --allow-warnings
if [ $? -ne 0 ]; then
exit $?
fi
done
else
bundle exec pod repo push "git@${GIT_REPO_PATH}.git" ${SRCROOT}/${MODULE_NAME}/${MODULE_NAME}.podspec "$@" --allow-warnings
fi