From 2dced7bfbfda5212e9b186f186362e7171184495 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Wed, 28 Nov 2018 17:19:01 +0900 Subject: [PATCH] Improve FloatingPanelController implementation --- Framework/Sources/FloatingPanel.swift | 19 +---- .../Sources/FloatingPanelController.swift | 80 +++++++++---------- Framework/Sources/FloatingPanelLayout.swift | 45 +++++++---- .../Sources/FloatingPanelTransitioning.swift | 9 ++- 4 files changed, 80 insertions(+), 73 deletions(-) diff --git a/Framework/Sources/FloatingPanel.swift b/Framework/Sources/FloatingPanel.swift index a3f776a..9c56ae2 100644 --- a/Framework/Sources/FloatingPanel.swift +++ b/Framework/Sources/FloatingPanel.swift @@ -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 diff --git a/Framework/Sources/FloatingPanelController.swift b/Framework/Sources/FloatingPanelController.swift index 4c92f8f..a567474 100644 --- a/Framework/Sources/FloatingPanelController.swift +++ b/Framework/Sources/FloatingPanelController.swift @@ -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 diff --git a/Framework/Sources/FloatingPanelLayout.swift b/Framework/Sources/FloatingPanelLayout.swift index 2ca4e82..a9e5ee4 100644 --- a/Framework/Sources/FloatingPanelLayout.swift +++ b/Framework/Sources/FloatingPanelLayout.swift @@ -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 { 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 { - 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 { + 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))") diff --git a/Framework/Sources/FloatingPanelTransitioning.swift b/Framework/Sources/FloatingPanelTransitioning.swift index 449de41..defb014 100644 --- a/Framework/Sources/FloatingPanelTransitioning.swift +++ b/Framework/Sources/FloatingPanelTransitioning.swift @@ -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) } } }