feature/bottom-sheet #11

Merged
ivan.smolin merged 15 commits from feature/bottom-sheet into master 2023-07-28 16:18:37 +03:00
Member

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 с левой кнопкой:

class CustomViewController: BaseModalViewController<UIView, UIView> {
    override var viewControllerAppearance: BaseAppearance {
        let appearance = super.viewControllerAppearance

        appearance.dragViewState = .presented(.defaultAppearance)
        appearance.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)
                                                    ])))
        })

        return appearance
    }
}

"Якори" контроллера

Раньше для настройки высоты контроллера необходимо было пользоваться свойствами longFormHeight, shortFormHeight. В базовом контроллере можно лишь передать список точек на которых контроллер должен будет задержаться:

class DetentsViewController: BaseModalViewController<UIView, UIView> {
    override var presentationDetents: [ModalViewPresentationDetent] {
        [.headerOnly, .height(300), .maxHeight]
    }
}
  • headerOnly будет сам пытаться вычеслить высоту хедера и dragView, показывая только их
  • height(_) будет показывать контроллер на переданной высоте
  • maxHeight - вся высота экрана (до safeArea)

В данный массив не рекомендуется передавать больше 3 значений, т.к. модальное окно все равно сможет занять только 3 положения на экране.

DimmedView и PassthroughDimmedView

Для контроля DimmedView (затемняющей view) есть отдельное свойство dimmedView. Эти классы позволяют настраивать поведение при тапе в затемнённую область и кастомизировать затемнение под ваши нужды.

class ShadowViewController: BaseModalViewController<UIView, UIView> {
    override var dimmedView: DimmedView? {
        let dimmedView = PassthroughDimmedView()
        dimmedView.hitTestHandlerView = view
        dimmedView.configureUIView(appearance: .init(shadow: UIViewShadow(radius: 8,
                                                                          color: .black,
                                                                          opacity: 0.3))

        return dimmedView
    }
}

Контроль закрытия

PanModalPresentable не умеет в настройку закрытия контроллера, делая это самостоятельно через dismiss(animated:completion:). Теперь можно настроить закрытие самостоятельно через свойства: onTapToDismiss и onDragToDismiss.

Взаимедействие с PanModal

Если нет необходимости или возможности использовать BaseModalViewController, вы все так же можете пользоваться протоколом PanModalRepresentable. Вот список изменений протокола:

  • Открытие/закрытие модального окна теперь можно настроить с помощью свойств onTapToDismiss и onDragToDismiss
  • Можно настроить промежуточное состояние модального окна с mediumFormHeight
  • DimmedView открыт для наследования и может создаваться в dimmedView у

Для BaseModalViewController все свойства из PanModalPresentable все также работают, т.е. вы можете их переопределять, добавлять и изменять по необходимости.

# 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 class CustomViewController: BaseModalViewController<UIView, UIView> { override var viewControllerAppearance: BaseAppearance { let appearance = super.viewControllerAppearance appearance.dragViewState = .presented(.defaultAppearance) appearance.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) ]))) }) return appearance } } ``` ## "Якори" контроллера Раньше для настройки высоты контроллера необходимо было пользоваться свойствами `longFormHeight`, `shortFormHeight`. В базовом контроллере можно лишь передать список точек на которых контроллер должен будет задержаться: ```swift class DetentsViewController: BaseModalViewController<UIView, UIView> { override var presentationDetents: [ModalViewPresentationDetent] { [.headerOnly, .height(300), .maxHeight] } } ``` - headerOnly будет сам пытаться вычеслить высоту хедера и dragView, показывая только их - height(_) будет показывать контроллер на переданной высоте - maxHeight - вся высота экрана (до safeArea) В данный массив не рекомендуется передавать больше 3 значений, т.к. модальное окно все равно сможет занять только 3 положения на экране. ## DimmedView и PassthroughDimmedView Для контроля `DimmedView` (затемняющей view) есть отдельное свойство `dimmedView`. Эти классы позволяют настраивать поведение при тапе в затемнённую область и кастомизировать затемнение под ваши нужды. ```swift class ShadowViewController: BaseModalViewController<UIView, UIView> { override var dimmedView: DimmedView? { let dimmedView = PassthroughDimmedView() dimmedView.hitTestHandlerView = view dimmedView.configureUIView(appearance: .init(shadow: UIViewShadow(radius: 8, color: .black, opacity: 0.3)) return dimmedView } } ``` ## Контроль закрытия `PanModalPresentable` не умеет в настройку закрытия контроллера, делая это самостоятельно через `dismiss(animated:completion:)`. Теперь можно настроить закрытие самостоятельно через свойства: `onTapToDismiss` и `onDragToDismiss`. # Взаимедействие с PanModal Если нет необходимости или возможности использовать `BaseModalViewController`, вы все так же можете пользоваться протоколом `PanModalRepresentable`. Вот список изменений протокола: - Открытие/закрытие модального окна теперь можно настроить с помощью свойств `onTapToDismiss` и `onDragToDismiss` - Можно настроить промежуточное состояние модального окна с `mediumFormHeight` - `DimmedView` открыт для наследования и может создаваться в `dimmedView` у > Для `BaseModalViewController` все свойства из `PanModalPresentable` все также работают, т.е. вы можете их переопределять, добавлять и изменять по необходимости.
nikita.semenov added 7 commits 2023-07-03 08:48:15 +03:00
3f112d2d26 Merge branch 'master' into feature/bottom-sheet
# Conflicts:
#	CHANGELOG.md
#	TIAppleMapUtils/TIAppleMapUtils.podspec
#	TIAuth/TIAuth.podspec
#	TIDeeplink/TIDeeplink.podspec
#	TIDeveloperUtils/TIDeveloperUtils.podspec
#	TIEcommerce/TIEcommerce.podspec
#	TIFoundationUtils/TIFoundationUtils.podspec
#	TIGoogleMapUtils/TIGoogleMapUtils.podspec
#	TIKeychainUtils/TIKeychainUtils.podspec
#	TILogging/TILogging.podspec
#	TIMapUtils/TIMapUtils.podspec
#	TIMoyaNetworking/TIMoyaNetworking.podspec
#	TINetworking/TINetworking.podspec
#	TINetworkingCache/TINetworkingCache.podspec
#	TIPagination/TIPagination.podspec
#	TISwiftUICore/TISwiftUICore.podspec
#	TISwiftUtils/TISwiftUtils.podspec
#	TITableKitUtils/TITableKitUtils.podspec
#	TITextProcessing/TITextProcessing.podspec
#	TIUIElements/Sources/Views/Helpers/WrappedViewLayout+Helpers.swift
#	TIUIElements/Sources/Views/Placeholder/Views/BasePlaceholderImageView.swift
#	TIUIElements/Sources/Views/Placeholder/Views/BasePlaceholderView.swift
#	TIUIElements/Sources/Wrappers/Constraints/CenterConstraints.swift
#	TIUIElements/TIUIElements.podspec
#	TIUIKitCore/TIUIKitCore.podspec
#	TIWebView/TIWebView.podspec
#	TIYandexMapUtils/TIYandexMapUtils.podspec
nikita.semenov added 1 commit 2023-07-03 10:17:52 +03:00
ivan.smolin reviewed 2023-07-03 14:43:01 +03:00
@ -0,0 +6,4 @@
pod 'TIUIElements', :path => '../../../../TIUIElements/TIUIElements.podspec'
pod 'TIUIKitCore', :path => '../../../../TIUIKitCore/TIUIKitCore.podspec'
pod 'TISwiftUtils', :path => '../../../../TISwiftUtils/TISwiftUtils.podspec'
Member

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

TIBottomSheet тоже надо добавить
ivan.smolin marked this conversation as resolved
@ -0,0 +123,4 @@
configureFooterViewLayout()
}
open override func bindViews() {
Member

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

а ты как-то учитываешь, что contentView.bounds может поменяться? типа мы обновили содержимое таблицы/коллекции?
ivan.smolin marked this conversation as resolved
@ -0,0 +340,4 @@
}
private func getFittingSize(forView view: UIView) -> CGSize {
let targetSize = CGSize(width: UIScreen.main.bounds.width,
Member

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

🤔 есть другие варианты?
ivan.smolin marked this conversation as resolved
@ -0,0 +27,4 @@
// MARK: - Default Values
public static var headerOnly: ModalViewPresentationDetent {
ModalViewPresentationDetent(height: CGFloat(Int.min))
Member
CGFLOAT_MIN? https://developer.apple.com/documentation/corefoundation/cgfloat_min
ivan.smolin marked this conversation as resolved
@ -0,0 +35,4 @@
}
public static var maxHeight: ModalViewPresentationDetent {
ModalViewPresentationDetent(height: CGFloat(Int.max))
Member

аналогично

аналогично
ivan.smolin marked this conversation as resolved
@ -0,0 +35,4 @@
case presented(UIView.BaseWrappedViewHolderAppearance<UIView.DefaultWrappedAppearance, UIView.DefaultWrappedLayout>)
}
func configureAppearance(appearance: UIView.BaseWrappedViewHolderAppearance<UIView.DefaultWrappedAppearance, UIView.DefaultWrappedLayout>) {
Member

configureModalFooter(appearance:)

configureModalFooter(appearance:)
Member

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

может some заиспользовать? чуть меньше кода, чуть больше гибкости в передаваемом параметре
ivan.smolin marked this conversation as resolved
@ -0,0 +37,4 @@
case none
case buttonLeft(BaseButtonStyle)
case buttonRight(BaseButtonStyle)
case buttons(left: BaseButtonStyle, right: BaseButtonStyle)
Member

лучше leading, trailing

лучше leading, trailing
ivan.smolin marked this conversation as resolved
@ -0,0 +53,4 @@
topConstraint: leftButton.topAnchor.constraint(equalTo: topAnchor),
bottomConstraint: leftButton.bottomAnchor.constraint(equalTo: bottomAnchor)),
centerConstraints: .init(centerYConstraint: leftButton.centerYAnchor.constraint(equalTo: centerYAnchor)),
sizeConstraints: .init())
Member

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

а почему бы не дать поддержку размеров?
ivan.smolin marked this conversation as resolved
@ -0,0 +133,4 @@
bottomConstraint: view.bottomAnchor.constraint(equalTo: bottomAnchor)),
centerConstraints: .init(centerXConstraint: view.centerXAnchor.constraint(equalTo: centerXAnchor),
centerYConstraint: view.centerYAnchor.constraint(equalTo: centerYAnchor)),
sizeConstraints: .init())
Member

и тут

и тут
ivan.smolin marked this conversation as resolved
@ -0,0 +137,4 @@
self.customViewConstraints = customViewConstraints
NSLayoutConstraint.deactivate(customViewConstraints.centerConstraints.allConstraints)
Member
customViewConstraints.centerConstraints.deactivate()
```swift customViewConstraints.centerConstraints.deactivate() ```
ivan.smolin marked this conversation as resolved
@ -0,0 +1,28 @@
Pod::Spec.new do |s|
s.name = 'TIBottomSheet'
s.version = '1.45.0'
Member

1.50.0

1.50.0
ivan.smolin marked this conversation as resolved
@ -133,3 +133,3 @@
middleViewLayout: WrappedViewLayout) {
let leadingToSuperviewContraint: NSLayoutConstraint
let leadingToSuperviewContraint: NSLayoutConstraint?
Member

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

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

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

153 строка: leadingViewConstraints.edgeConstraints.leadingConstraint - опциональный констрейнт
ivan.smolin marked this conversation as resolved
@ -48,0 +47,4 @@
centerYConstraint?.setActiveConstantOrDeactivate(constant: offset.vertical)
}
public var centerConstraints: [NSLayoutConstraint] {
Member

а чем отличается от allConstraints?

а чем отличается от allConstraints?
ivan.smolin marked this conversation as resolved
@ -0,0 +22,4 @@
import UIKit
public final class ScrollViewWrapper<ContentView: UIView>: UIScrollView {
Member

из названия создаётся впечатление, что мы оборачиваем scrollView

из названия создаётся впечатление, что мы оборачиваем scrollView
ivan.smolin marked this conversation as resolved
@ -0,0 +43,4 @@
])
}
required init?(coder: NSCoder) {
Member

unavailable?

unavailable?
ivan.smolin marked this conversation as resolved
ivan.smolin added 2 commits 2023-07-24 20:15:12 +03:00
ivan.smolin force-pushed feature/bottom-sheet from ba4cdc6e0e to 27d5a3a9ca 2023-07-24 20:48:53 +03:00 Compare
ivan.smolin added 1 commit 2023-07-25 17:33:46 +03:00
ivan.smolin added 1 commit 2023-07-26 10:25:41 +03:00
ivan.smolin added 1 commit 2023-07-26 14:37:55 +03:00
ivan.smolin force-pushed feature/bottom-sheet from d55745efc6 to 35ec61a0ee 2023-07-26 17:21:22 +03:00 Compare
ivan.smolin force-pushed feature/bottom-sheet from 35ec61a0ee to 843a887ec7 2023-07-26 17:31:34 +03:00 Compare
ivan.smolin added 1 commit 2023-07-26 22:24:07 +03:00
vladimir.makarov reviewed 2023-07-28 11:16:58 +03:00
@ -0,0 +65,4 @@
}
extension ContainerScrollView: AppearanceConfigurable where View: AppearanceConfigurable,
View.Appearance: WrappedViewAppearance {

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

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

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

Автовыравнивание, я бы не фокусировался на этих мелочах
vladimir.makarov marked this conversation as resolved
vladimir.makarov reviewed 2023-07-28 11:17:44 +03:00
@ -0,0 +108,4 @@
`PanModalPresentable` не умеет в настройку закрытия контроллера, делая это самостоятельно через `dismiss(animated:completion:)`. Теперь можно настроить закрытие самостоятельно через свойства: `onTapToDismiss` и `onDragToDismiss`.
# Взаимедействие с PanModal

Взаимодействие

Взаимодействие
vladimir.makarov marked this conversation as resolved
vladimir.makarov reviewed 2023-07-28 11:18:02 +03:00
@ -20,3 +19,1 @@
exit $?
fi
done
GIT_REPO_PATH="github.com:petropavel13/TestPodspecs"

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

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

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

утёк тестовый код)
vladimir.makarov marked this conversation as resolved

И еще, я, возможно, пропустил, но можно изменить состояние модалки через свойство / метод? Типо modalController.state = .maxHeight или modalController.updateState(to: .maiHeight), что-то вот такое?

И еще, я, возможно, пропустил, но можно изменить состояние модалки через свойство / метод? Типо `modalController.state = .maxHeight` или `modalController.updateState(to: .maiHeight)`, что-то вот такое?
ivan.smolin added 1 commit 2023-07-28 16:08:45 +03:00
vladimir.makarov approved these changes 2023-07-28 16:17:25 +03:00
ivan.smolin merged commit b8611321fb into master 2023-07-28 16:18:37 +03:00
ivan.smolin deleted branch feature/bottom-sheet 2023-07-28 16:18:38 +03:00
Sign in to join this conversation.
No reviewers
No Label
No Milestone
No project
No Assignees
3 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: TouchInstinct/LeadKit#11
No description provided.