fix: move presentation detents settings to modal view controller appearance, force use nan for undefined layout dimension, fix related layout issues

This commit is contained in:
Ivan Smolin 2023-07-26 14:37:49 +03:00
parent 8007532351
commit 35ec61a0ee
15 changed files with 153 additions and 122 deletions

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,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/Moya/Moya.git",
"state" : {
"revision" : "9b906860e3c3c09032879465c471e6375829593f",
"version" : "15.0.0"
"revision" : "c263811c1f3dbf002be9bd83107f7cdc38992b26",
"version" : "15.0.3"
}
},
{
@ -59,8 +59,8 @@
"kind" : "remoteSourceControl",
"location" : "https://git.svc.touchin.ru/TouchInstinct/PanModal",
"state" : {
"revision" : "be82eddb529faa2bc668230906ec007c53e7b635",
"version" : "1.3.0"
"revision" : "ced7c1703f90746df0224b6e0d33c146d9ae4284",
"version" : "1.3.1"
}
},
{
@ -77,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

@ -28,6 +28,7 @@ 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
@ -36,15 +37,26 @@ extension BaseModalViewController {
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)
super.init(layout: layout,
backgroundColor: backgroundColor,
border: border,
shadow: shadow)
}
}
public final class DefaultAppearance: BaseAppearance, ViewAppearance {
public static var defaultAppearance: Self {
Self()
}
}
}

View File

@ -112,7 +112,7 @@ open class BaseModalViewController<ContentView: UIView,
let edgeConstraints = EdgeConstraints(leadingConstraint: leadingConstraint,
trailingConstraint: trailingConstraint,
topConstraint: headerBottomToContentTopConstraint,
bottomConstraint: contentView.bottomAnchor.constraint(equalTo: view.bottomAnchor))
bottomConstraint: contentViewBottomToSuperviewConstraint)
let centerXConstraint = contentView.centerXAnchor.constraint(equalTo: view.centerXAnchor)
let centerYConstraint = contentView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
@ -153,9 +153,7 @@ open class BaseModalViewController<ContentView: UIView,
// MARK: - Modal View Controller Configuration
open var viewControllerAppearance: BaseAppearance {
.init(backgroundColor: .white)
}
public var viewControllerAppearance: BaseAppearance = .init(backgroundColor: .white)
open var panScrollable: UIScrollView? {
contentView as? UIScrollView
@ -167,9 +165,9 @@ open class BaseModalViewController<ContentView: UIView,
}
}
open var presentationDetents: [ModalViewPresentationDetent] {
[.maxHeight]
}
public var dimmedView = DimmedView()
public var showDragIndicator = true
open var headerViewHeight: CGFloat {
let dragViewHeight = getHeight(of: dragView)
@ -346,8 +344,10 @@ open class BaseModalViewController<ContentView: UIView,
of: headerViewAppearance.layout.insets)
} else {
dragViewBottomToHeaderViewTopConstraint.isActive = false
topConstraint = dragViewBottomToContentViewTopConstraint
topConstant = headerViewAppearance.layout.insets.top
topConstraint = headerViewToSuperviewTopConstraint
let topInset = headerViewAppearance.layout.insets.top
topConstant = topInset.isFinite ? topInset : .zero
}
headerViewConstraints.edgeConstraints.topConstraint = topConstraint
@ -380,8 +380,11 @@ open class BaseModalViewController<ContentView: UIView,
topConstant = .zero
}
topConstraint.setActiveConstantOrDeactivate(constant: topConstant)
contentViewConstraints.activate()
contentViewConstraints.edgeConstraints.topConstraint = topConstraint
let layout = UIView.DefaultWrappedLayout(insets: .horizontal(.zero)
.vertical(top: topConstant)
.replacingNan(with: .zero))
contentViewConstraints.update(from: layout)
}
private func configureFooterViewLayout() {
@ -389,6 +392,9 @@ open class BaseModalViewController<ContentView: UIView,
return
}
contentViewConstraints.edgeConstraints.bottomConstraint.isActive = false
contentViewConstraints.edgeConstraints.bottomConstraint = footerViewConstraints.edgeConstraints.topConstraint
footerViewConstraints.update(from: footerViewAppearance.layout)
}
@ -423,7 +429,7 @@ open class BaseModalViewController<ContentView: UIView,
}
private func getSortedDetents() -> [ModalViewPresentationDetent] {
presentationDetents.uniqued().sorted()
viewControllerAppearance.presentationDetents.uniqued().sorted()
}
private func getHeight(of view: UIView) -> CGFloat {

View File

@ -30,6 +30,9 @@ open class DefaultModalWrapperViewController<ContentController: UIViewController
self.contentViewController = contentViewController
super.init(nibName: nil, bundle: nil)
addChild(contentViewController)
contentViewController.didMove(toParent: self)
}
@available(*, unavailable)
@ -38,6 +41,10 @@ open class DefaultModalWrapperViewController<ContentController: UIViewController
super.init(coder: coder)
}
open override func createContentView() -> UIView {
contentViewController.view
}
}
public extension UIViewController {

View File

@ -53,16 +53,12 @@ public final class DragView: BaseInitializableView, AppearanceConfigurable {
public final class Appearance: UIView.BaseWrappedAppearance<UIView.DefaultWrappedLayout>, WrappedViewAppearance {
public static var defaultAppearance: Appearance {
.make { dragView in
dragView.backgroundColor = Constants.dragViewBackgroundColor
dragView.border = Constants.dragViewBorder
dragView.layout { layout in
layout.size = Constants.dragViewSize
layout.insets = .vertical(top: Constants.dragViewTopInset)
}
}
public static var defaultAppearance: Self {
Self(layout: DefaultWrappedLayout(insets: .vertical(top: Constants.dragViewTopInset),
size: Constants.dragViewSize,
centerOffset: .centerHorizontal()),
backgroundColor: Constants.dragViewBackgroundColor,
border: Constants.dragViewBorder)
}
}

View File

@ -29,6 +29,17 @@ open class ModalFooterView<ContentView: UIView>: ContainerView<ContentView> {
public enum State {
case hidden
case presented(BaseWrappedViewHolderAppearance<DefaultWrappedAppearance, DefaultWrappedLayout>)
case presented(Appearance)
}
}
public extension ModalFooterView {
final class Appearance: BaseWrappedViewHolderAppearance<DefaultWrappedAppearance, DefaultWrappedLayout>,
WrappedViewHolderAppearance {
public static var defaultAppearance: Self {
Self(layout: DefaultWrappedLayout(insets: .horizontal(.zero).vertical(bottom: .zero),
size: .fixedHeight(44)))
}
}
}

View File

@ -53,7 +53,7 @@ open class ModalHeaderView: BaseInitializableView, AppearanceConfigurable {
leadingButton.trailingAnchor.constraint(equalTo: trailingButton.leadingAnchor)
}()
public private(set) lazy var leftButtonConstraints: SubviewConstraints = {
public private(set) lazy var leadingButtonConstraints: SubviewConstraints = {
let edgeConstraints = EdgeConstraints(leadingConstraint: leadingButton.leadingAnchor.constraint(equalTo: leadingAnchor),
trailingConstraint: leftTrailingToRightLeadingConstraint,
topConstraint: leadingButton.topAnchor.constraint(equalTo: topAnchor),
@ -77,11 +77,10 @@ open class ModalHeaderView: BaseInitializableView, AppearanceConfigurable {
trailingButton.leadingAnchor.constraint(equalTo: leadingAnchor)
}()
public private(set) lazy var rightButtonConstraints: SubviewConstraints = {
let leadingConstraint = trailingButton.leadingAnchor.constraint(equalTo: leadingAnchor)
public private(set) lazy var trailingButtonConstraints: SubviewConstraints = {
let trailingConstraint = trailingButton.trailingAnchor.constraint(equalTo: trailingAnchor)
let edgeConstraints = EdgeConstraints(leadingConstraint: leadingConstraint,
let edgeConstraints = EdgeConstraints(leadingConstraint: leftTrailingToRightLeadingConstraint,
trailingConstraint: trailingConstraint,
topConstraint: trailingButton.topAnchor.constraint(equalTo: topAnchor),
bottomConstraint: trailingButton.bottomAnchor.constraint(equalTo: bottomAnchor))
@ -133,31 +132,34 @@ open class ModalHeaderView: BaseInitializableView, AppearanceConfigurable {
switch state {
case let .leadingButton(style):
leadingButtonStyle = style
leftButtonConstraints.edgeConstraints.trailingConstraint = leftTrailingToSuperviewTrailing
leadingButtonConstraints.edgeConstraints.trailingConstraint = leftTrailingToSuperviewTrailing
case let .trailingButton(style):
trailingButtonStyle = style
rightButtonConstraints.edgeConstraints.leadingConstraint = rightLeadingToSuperviewLeadingConstraint
trailingButtonConstraints.edgeConstraints.leadingConstraint = rightLeadingToSuperviewLeadingConstraint
case let .buttons(leading, trailing):
leadingButtonStyle = leading
trailingButtonStyle = trailing
leftButtonConstraints.edgeConstraints.trailingConstraint = leftTrailingToRightLeadingConstraint
rightButtonConstraints.edgeConstraints.leadingConstraint = leftTrailingToRightLeadingConstraint
let spacing = leading.appearance.layout.insets.add(\.right,
to: \.left,
of: trailing.appearance.layout.insets)
leftTrailingToRightLeadingConstraint.setActiveConstantOrDeactivate(constant: spacing)
case let .custom(view, appearance):
configureCustomView(view, withLayout: appearance.layout)
view.configureUIView(appearance: appearance)
}
configure(buttonStyle: leadingButtonStyle, forButton: leadingButton, constraints: leftButtonConstraints)
configure(buttonStyle: trailingButtonStyle, forButton: trailingButton, constraints: rightButtonConstraints)
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
@ -210,7 +212,8 @@ public extension ModalHeaderView {
final class Appearance: BaseWrappedAppearance<DefaultWrappedLayout>, WrappedViewAppearance {
public static var defaultAppearance: Self {
Self()
Self(layout: DefaultWrappedLayout(insets: .horizontal(.zero).vertical(top: .zero),
size: .fixedHeight(44)))
}
public var contentViewState: ContentViewState

View File

@ -24,7 +24,7 @@ import UIKit
import PanModal
open class PassthroughDimmedView: DimmedView {
weak var hitTestHandlerView: UIView?
public weak var hitTestHandlerView: UIView?
public var isTransparent = false

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

@ -184,7 +184,8 @@ open class BaseListItemView<LeadingView: UIView,
leadingViewLeftInset = leadingViewLayout.insets.left
}
leadingToSuperviewContraint.setActiveConstantOrDeactivate(constant: leadingViewLeftInset)
leadingToSuperviewContraint.constant = leadingViewLeftInset.isFinite ? leadingViewLeftInset : .zero
leadingToSuperviewContraint.isActive = true
}
private func update(middleViewLayout: WrappedViewLayout) {
@ -219,6 +220,7 @@ open class BaseListItemView<LeadingView: UIView,
trailingViewRightInset = trailingViewLayout.insets.right
}
trailingToSuperviewConstraint.setActiveConstantOrDeactivate(constant: -trailingViewRightInset)
trailingToSuperviewConstraint.constant = trailingViewRightInset.isFinite ? -trailingViewRightInset : .zero
trailingToSuperviewConstraint.isActive = true
}
}

View File

@ -47,14 +47,6 @@ 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

View File

@ -52,7 +52,7 @@ open class CollectionTableViewCell<CollectionView: UICollectionView>: ContainerT
}
let cachedCollectionFrame = wrappedView.frame
wrappedView.frame.size.width = targetSize.width - wrappedContentInsets.horizontal(onNan: .zero)
wrappedView.frame.size.width = max(targetSize.width - wrappedContentInsets.horizontal(onNan: .zero), .zero)
let collectionContentHeight = wrappedView.collectionViewLayout.collectionViewContentSize.height
wrappedView.frame = cachedCollectionFrame

View File

@ -26,6 +26,10 @@ 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)
}
@ -64,14 +68,21 @@ 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,