Improve FloatingPanelController implementation

This commit is contained in:
Shin Yamamoto
2018-12-06 09:11:25 +09:00
parent ac9f8fe89c
commit 2dced7bfbf
4 changed files with 80 additions and 73 deletions
+4 -15
View File
@@ -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
+40 -40
View File
@@ -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
+28 -17
View File
@@ -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)
}
}
}