Improve FloatingPanelController implementation
This commit is contained in:
@@ -38,7 +38,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
}
|
||||
|
||||
private var isBottomState: Bool {
|
||||
let remains = layoutAdapter.layout.supportedPositions.filter { $0.rawValue > state.rawValue }
|
||||
let remains = layoutAdapter.supportedPositions.filter { $0.rawValue > state.rawValue }
|
||||
return remains.count == 0
|
||||
}
|
||||
|
||||
@@ -115,17 +115,6 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
move(from: state, to: to, animated: animated, completion: completion)
|
||||
}
|
||||
|
||||
func present(animated: Bool, completion: (() -> Void)? = nil) {
|
||||
if animated {
|
||||
self.layoutAdapter.activateLayout(of: .hidden)
|
||||
}
|
||||
move(from: .hidden, to: layoutAdapter.layout.initialPosition, animated: animated, completion: completion)
|
||||
}
|
||||
|
||||
func dismiss(animated: Bool, completion: (() -> Void)? = nil) {
|
||||
move(from: state, to: .hidden, animated: animated, completion: completion)
|
||||
}
|
||||
|
||||
private func move(from: FloatingPanelPosition, to: FloatingPanelPosition, animated: Bool, completion: (() -> Void)? = nil) {
|
||||
if to != .full {
|
||||
lockScrollView()
|
||||
@@ -574,7 +563,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
private func directionalPosition(with translation: CGPoint) -> FloatingPanelPosition {
|
||||
let currentY = getCurrentY(from: initialFrame, with: translation)
|
||||
|
||||
let supportedPositions: Set = layoutAdapter.layout.supportedPositions
|
||||
let supportedPositions = layoutAdapter.supportedPositions
|
||||
|
||||
if supportedPositions.count == 1 {
|
||||
return state
|
||||
@@ -609,7 +598,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
private func redirectionalPosition(with translation: CGPoint) -> FloatingPanelPosition {
|
||||
let currentY = getCurrentY(from: initialFrame, with: translation)
|
||||
|
||||
let supportedPositions: Set = layoutAdapter.layout.supportedPositions
|
||||
let supportedPositions = layoutAdapter.supportedPositions
|
||||
|
||||
if supportedPositions.count == 1 {
|
||||
return state
|
||||
@@ -644,7 +633,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate
|
||||
|
||||
private func targetPosition(with translation: CGPoint, velocity: CGPoint) -> (FloatingPanelPosition) {
|
||||
let currentY = getCurrentY(from: initialFrame, with: translation)
|
||||
let supportedPositions: Set = layoutAdapter.layout.supportedPositions
|
||||
let supportedPositions = layoutAdapter.supportedPositions
|
||||
|
||||
if supportedPositions.count == 1 {
|
||||
return state
|
||||
|
||||
@@ -126,7 +126,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
|
||||
private var _contentViewController: UIViewController?
|
||||
|
||||
private var floatingPanel: FloatingPanel!
|
||||
private var layoutInsetsObservations: [NSKeyValueObservation] = []
|
||||
private var safeAreaInsetsObservation: NSKeyValueObservation?
|
||||
private let modalTransition = FloatingPanelModalTransition()
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
@@ -151,6 +151,8 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
|
||||
behavior: fetchBehavior(for: self.traitCollection))
|
||||
}
|
||||
|
||||
// MARK:- Overrides
|
||||
|
||||
/// Creates the view that the controller manages.
|
||||
override public func loadView() {
|
||||
assert(self.storyboard == nil, "Storyboard isn't supported")
|
||||
@@ -171,6 +173,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
|
||||
|
||||
// Change layout for a new trait collection
|
||||
updateLayout(for: newCollection)
|
||||
floatingPanel.layoutAdapter.checkLayoutConsistance()
|
||||
|
||||
floatingPanel.behavior = fetchBehavior(for: newCollection)
|
||||
}
|
||||
@@ -184,16 +187,31 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
|
||||
|
||||
public override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
// Need to update safeAreaInsets here to ensure that the `adjustedContentInsets` has a correct value.
|
||||
// Because the `viewSafeAreaInsetsDidChange()` isn't called expectedly and
|
||||
// `view.safeAreaInsets` has a correct value of the bottom inset here.
|
||||
self.update(safeAreaInsets: layoutInsets)
|
||||
if layout is FloatingPanelIntrinsicLayout {
|
||||
updateLayout()
|
||||
// Must track safeAreaInsets/{top,bottom}LayoutGuide of the `self.view`
|
||||
// to update floatingPanel.safeAreaInsets`. There are 2 reasons.
|
||||
// 1. This or the parent VC doesn't call viewSafeAreaInsetsDidChange() on the bottom
|
||||
// inset's update expectedly.
|
||||
// 2. The safe area top inset can be variable on the large title navigation bar(iOS11+).
|
||||
// That's why it needs the observation to keep `adjustedContentInsets` correct.
|
||||
if #available(iOS 11.0, *) {
|
||||
safeAreaInsetsObservation = self.observe(\.view.safeAreaInsets) { [weak self] (vc, chaneg) in
|
||||
guard let self = self else { return }
|
||||
self.update(safeAreaInsets: vc.layoutInsets)
|
||||
}
|
||||
} else {
|
||||
// KVOs for topLayoutGuide & bottomLayoutGuide are not effective.
|
||||
// Instead, safeAreaInsets is updated here
|
||||
self.update(safeAreaInsets: layoutInsets)
|
||||
}
|
||||
}
|
||||
|
||||
public override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
safeAreaInsetsObservation = nil
|
||||
}
|
||||
|
||||
// MARK:- Privates
|
||||
|
||||
private func fetchLayout(for traitCollection: UITraitCollection) -> FloatingPanelLayout {
|
||||
switch traitCollection.verticalSizeClass {
|
||||
case .compact:
|
||||
@@ -226,45 +244,26 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
|
||||
|
||||
private func updateLayout(for traitCollection: UITraitCollection) {
|
||||
floatingPanel.layoutAdapter.layout = fetchLayout(for: traitCollection)
|
||||
floatingPanel.layoutAdapter.checkLayoutConsistance()
|
||||
floatingPanel.layoutAdapter.prepareLayout(in: self)
|
||||
floatingPanel.layoutAdapter.activateLayout(of: floatingPanel.state)
|
||||
}
|
||||
|
||||
// MARK: - Container view controller interface
|
||||
|
||||
/// Shows the surface view at the initial position defined by the current layout
|
||||
public func show(animated: Bool = false, completion: (() -> Void)? = nil) {
|
||||
layoutInsetsObservations.removeAll()
|
||||
|
||||
// Must track safeAreaInsets/{top,bottom}LayoutGuide of the `self.view`
|
||||
// to update floatingPanel.safeAreaInsets`. There are 2 reasons.
|
||||
// 1. This or the parent VC doesn't call viewSafeAreaInsetsDidChange() on the bottom
|
||||
// inset's update expectedly.
|
||||
// 2. The safe area top inset can be variable on the large title navigation bar.
|
||||
// That's why it needs the observation to keep `adjustedContentInsets` correct.
|
||||
if #available(iOS 11.0, *) {
|
||||
let observaion = self.observe(\.view.safeAreaInsets) { [weak self] (vc, chaneg) in
|
||||
guard let self = self else { return }
|
||||
self.update(safeAreaInsets: vc.layoutInsets)
|
||||
}
|
||||
layoutInsetsObservations.append(observaion)
|
||||
} else {
|
||||
// KVOs for topLayoutGuide & bottomLayoutGuide are not effective.
|
||||
// Instead, safeAreaInsets will be updated in viewDidAppear()
|
||||
}
|
||||
|
||||
// Must set a layout again here because `self.traitCollection` is applied correctly once it's added to a parent VC
|
||||
floatingPanel.layoutAdapter.layout = fetchLayout(for: traitCollection)
|
||||
floatingPanel.layoutAdapter.prepareLayout(in: self)
|
||||
|
||||
floatingPanel.behavior = fetchBehavior(for: traitCollection)
|
||||
|
||||
floatingPanel.present(animated: animated, completion: completion)
|
||||
// Must apply the current layout here
|
||||
updateLayout(for: traitCollection)
|
||||
move(to: floatingPanel.layoutAdapter.layout.initialPosition,
|
||||
animated: animated,
|
||||
completion: completion)
|
||||
}
|
||||
|
||||
/// Hides the surface view to the hidden position
|
||||
public func hide(animated: Bool = false, completion: (() -> Void)? = nil) {
|
||||
layoutInsetsObservations.removeAll()
|
||||
floatingPanel.dismiss(animated: animated, completion: completion)
|
||||
move(to: .hidden,
|
||||
animated: animated,
|
||||
completion: completion)
|
||||
}
|
||||
|
||||
/// Adds the view managed by the controller as a child of the specified view controller.
|
||||
@@ -299,7 +298,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
|
||||
|
||||
parent.addChild(self)
|
||||
|
||||
self.show(animated: animated) { [weak self] in
|
||||
show(animated: true) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.didMove(toParent: parent)
|
||||
}
|
||||
@@ -317,7 +316,6 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
|
||||
|
||||
hide(animated: animated) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
self.willMove(toParent: nil)
|
||||
self.view.removeFromSuperview()
|
||||
self.removeFromParent()
|
||||
@@ -331,6 +329,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
|
||||
/// - animated: Pass true to animate the presentation; otherwise, pass false.
|
||||
/// - completion: The block to execute after the view controller has finished moving. This block has no return value and takes no parameters. You may specify nil for this parameter.
|
||||
public func move(to: FloatingPanelPosition, animated: Bool, completion: (() -> Void)? = nil) {
|
||||
precondition(floatingPanel.layoutAdapter.vc != nil, "Use show(animated:completion)")
|
||||
floatingPanel.move(to: to, animated: animated, completion: completion)
|
||||
}
|
||||
|
||||
@@ -351,7 +350,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
|
||||
|
||||
_contentViewController = contentViewController
|
||||
}
|
||||
|
||||
|
||||
@available(*, unavailable, renamed: "set(contentViewController:)")
|
||||
public override func show(_ vc: UIViewController, sender: Any?) {
|
||||
if let target = self.parent?.targetViewController(forAction: #selector(UIViewController.show(_:sender:)), sender: sender) {
|
||||
@@ -404,6 +403,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI
|
||||
/// animation block.
|
||||
public func updateLayout() {
|
||||
updateLayout(for: view.traitCollection)
|
||||
floatingPanel.layoutAdapter.checkLayoutConsistance()
|
||||
}
|
||||
|
||||
/// Returns the y-coordinate of the point at the origin of the surface view
|
||||
|
||||
@@ -22,7 +22,11 @@ public protocol FloatingPanelLayout: class {
|
||||
/// Returns the initial position of a floating panel.
|
||||
var initialPosition: FloatingPanelPosition { get }
|
||||
|
||||
/// Returns a set of FloatingPanelPosition objects to tell the applicable positions of the floating panel controller. Default is all of them.
|
||||
/// Returns a set of FloatingPanelPosition objects to tell the applicable
|
||||
/// positions of the floating panel controller.
|
||||
///
|
||||
/// By default, it returns all position exepct for `hidden` position. Because
|
||||
/// it's always supported by `FloatingPanelController` so you don't need to return it.
|
||||
var supportedPositions: Set<FloatingPanelPosition> { get }
|
||||
|
||||
/// Return the interaction buffer to the top from the top position. Default is 6.0.
|
||||
@@ -31,10 +35,13 @@ public protocol FloatingPanelLayout: class {
|
||||
/// Return the interaction buffer to the bottom from the bottom position. Default is 6.0.
|
||||
var bottomInteractionBuffer: CGFloat { get }
|
||||
|
||||
/// Returns a CGFloat value to determine a floating panel height for each position(full, half and tip).
|
||||
/// A value for full position indicates a top inset from a safe area.
|
||||
/// On the other hand, values for half and tip positions indicate bottom insets from a safe area.
|
||||
/// If a position doesn't contain the supported positions, return nil.
|
||||
/// Returns a CGFloat value to determine a Y coordinate of a floating panel for each position(full, half, tip and hidden).
|
||||
///
|
||||
/// Its returning value indicates a different inset for each positiion.
|
||||
/// For full position, a top inset from a safe area in `FloatingPanelController.view`.
|
||||
/// For half or tip position, a bottom inset from the safe area.
|
||||
/// For hidden position, a bottom inset from `FloatingPanelController.view`.
|
||||
/// If a position isn't supported or the default value is used, return nil.
|
||||
func insetFor(position: FloatingPanelPosition) -> CGFloat?
|
||||
|
||||
/// Returns X-axis and width layout constraints of the surface view of a floating panel.
|
||||
@@ -54,7 +61,7 @@ public extension FloatingPanelLayout {
|
||||
var bottomInteractionBuffer: CGFloat { return 6.0 }
|
||||
|
||||
var supportedPositions: Set<FloatingPanelPosition> {
|
||||
return Set(FloatingPanelPosition.allCases)
|
||||
return Set([.full, .half, .tip])
|
||||
}
|
||||
|
||||
func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] {
|
||||
@@ -107,7 +114,7 @@ public class FloatingPanelDefaultLandscapeLayout: FloatingPanelLayout {
|
||||
|
||||
|
||||
class FloatingPanelLayoutAdapter {
|
||||
private weak var vc: UIViewController!
|
||||
weak var vc: UIViewController!
|
||||
private weak var surfaceView: FloatingPanelSurfaceView!
|
||||
private weak var backdropView: FloatingPanelBackdropView!
|
||||
|
||||
@@ -115,8 +122,9 @@ class FloatingPanelLayoutAdapter {
|
||||
|
||||
var safeAreaInsets: UIEdgeInsets = .zero {
|
||||
didSet {
|
||||
updateHeight()
|
||||
checkLayoutConsistance()
|
||||
if oldValue != safeAreaInsets {
|
||||
updateHeight()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,11 +146,17 @@ class FloatingPanelLayoutAdapter {
|
||||
return layout.insetFor(position: .tip) ?? 0.0
|
||||
}
|
||||
private var hiddenInset: CGFloat {
|
||||
return layout.insetFor(position: .hidden) ?? -safeAreaInsets.bottom
|
||||
return layout.insetFor(position: .hidden) ?? 0.0
|
||||
}
|
||||
|
||||
var supportedPositions: Set<FloatingPanelPosition> {
|
||||
var supportedPositions = layout.supportedPositions
|
||||
supportedPositions.remove(.hidden)
|
||||
return supportedPositions
|
||||
}
|
||||
|
||||
var topY: CGFloat {
|
||||
if layout.supportedPositions.contains(.full) {
|
||||
if supportedPositions.contains(.full) {
|
||||
return (safeAreaInsets.top + fullInset)
|
||||
} else {
|
||||
return middleY
|
||||
@@ -154,7 +168,7 @@ class FloatingPanelLayoutAdapter {
|
||||
}
|
||||
|
||||
var bottomY: CGFloat {
|
||||
if layout.supportedPositions.contains(.tip) {
|
||||
if supportedPositions.contains(.tip) {
|
||||
return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + tipInset)
|
||||
} else {
|
||||
return middleY
|
||||
@@ -227,7 +241,7 @@ class FloatingPanelLayoutAdapter {
|
||||
constant: -tipInset),
|
||||
]
|
||||
offConstraints = [
|
||||
surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.bottomAnchor,
|
||||
surfaceView.topAnchor.constraint(equalTo: vc.view.bottomAnchor,
|
||||
constant: -hiddenInset),
|
||||
]
|
||||
}
|
||||
@@ -266,8 +280,7 @@ class FloatingPanelLayoutAdapter {
|
||||
|
||||
NSLayoutConstraint.activate(fixedConstraints)
|
||||
|
||||
let supportedPositions = layout.supportedPositions.union([.hidden])
|
||||
if supportedPositions.contains(state) == false {
|
||||
if supportedPositions.union([.hidden]).contains(state) == false {
|
||||
state = layout.initialPosition
|
||||
}
|
||||
|
||||
@@ -298,8 +311,6 @@ class FloatingPanelLayoutAdapter {
|
||||
|
||||
func checkLayoutConsistance() {
|
||||
// Verify layout configurations
|
||||
let supportedPositions = layout.supportedPositions
|
||||
|
||||
assert(supportedPositions.count > 0)
|
||||
assert(supportedPositions.contains(layout.initialPosition),
|
||||
"Does not include an initial potision(\(layout.initialPosition)) in supportedPositions(\(supportedPositions))")
|
||||
|
||||
@@ -43,7 +43,14 @@ class FloatingPanelPresentationController: UIPresentationController {
|
||||
override func presentationTransitionDidEnd(_ completed: Bool) {
|
||||
if let fpc = presentedViewController as? FloatingPanelController{
|
||||
// For non-animated presentation
|
||||
fpc.show(animated: false)
|
||||
fpc.show(animated: false, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
override func dismissalTransitionDidEnd(_ completed: Bool) {
|
||||
if let fpc = presentedViewController as? FloatingPanelController{
|
||||
// For non-animated presentation
|
||||
fpc.hide(animated: false, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user