From 4c4a3af9cb3c3d1edc6e458e3b18cbdfcb49ec41 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Tue, 23 Oct 2018 10:22:48 +0900 Subject: [PATCH 001/623] Fix README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index de4d47fd..384526b0 100644 --- a/README.md +++ b/README.md @@ -177,7 +177,7 @@ class FloatingPanelStocksBehavior: FloatingPanelBehavior { return 15.0 } - func interactionAnimator(to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator { + func interactionAnimator(_ fpc: FloatingPanelController, to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator { let damping = self.damping(with: velocity) let springTiming = UISpringTimingParameters(dampingRatio: damping, initialVelocity: velocity) return UIViewPropertyAnimator(duration: 0.5, timingParameters: springTiming) From 570219a43d78a292f70e5f770fc463fd0504d95a Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Tue, 23 Oct 2018 14:20:28 +0900 Subject: [PATCH 002/623] Update Samples App --- .../Sources/Base.lproj/Main.storyboard | 22 ++++++++++++++++++- Examples/Samples/Sources/UIComponents.swift | 15 +++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/Examples/Samples/Sources/Base.lproj/Main.storyboard b/Examples/Samples/Sources/Base.lproj/Main.storyboard index 74116304..13c9653a 100644 --- a/Examples/Samples/Sources/Base.lproj/Main.storyboard +++ b/Examples/Samples/Sources/Base.lproj/Main.storyboard @@ -134,11 +134,31 @@ + + + + + + + + + + + + + + + + + + + + @@ -149,7 +169,7 @@ - + diff --git a/Examples/Samples/Sources/UIComponents.swift b/Examples/Samples/Sources/UIComponents.swift index 836ddbdc..dda2079d 100644 --- a/Examples/Samples/Sources/UIComponents.swift +++ b/Examples/Samples/Sources/UIComponents.swift @@ -86,3 +86,18 @@ class SafeAreaView: UIView { ]) } } + + +@IBDesignable +class OnSafeAreaView: UIView { + override func prepareForInterfaceBuilder() { + let label = UILabel() + label.text = "On Safe Area" + addSubview(label) + label.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + label.centerXAnchor.constraint(equalTo: self.centerXAnchor), + label.centerYAnchor.constraint(equalTo: self.centerYAnchor, constant: -4.0), + ]) + } +} From fd772aa56ebb33b63e65c168cf16194f1ad4bb3f Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Tue, 23 Oct 2018 14:13:00 +0900 Subject: [PATCH 003/623] Match the bottoms of the surface view and a device bottom --- Framework/Sources/FloatingPanelLayout.swift | 16 ++++++++++------ Framework/Sources/FloatingPanelSurfaceView.swift | 6 +++++- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/Framework/Sources/FloatingPanelLayout.swift b/Framework/Sources/FloatingPanelLayout.swift index c5fee0bf..aaf3883f 100644 --- a/Framework/Sources/FloatingPanelLayout.swift +++ b/Framework/Sources/FloatingPanelLayout.swift @@ -127,7 +127,7 @@ class FloatingPanelLayoutAdapter { var adjustedContentInsets: UIEdgeInsets { return UIEdgeInsets(top: 0.0, left: 0.0, - bottom: (safeAreaInsets.top + topInset) + (heightBuffer + safeAreaInsets.bottom), + bottom: safeAreaInsets.bottom, right: 0.0) } @@ -202,12 +202,16 @@ class FloatingPanelLayoutAdapter { } } - if let heightConstraints = self.heightConstraints { - NSLayoutConstraint.deactivate([heightConstraints]) + if let consts = self.heightConstraints { + NSLayoutConstraint.deactivate([consts]) } - let heightConstraints = surfaceView.heightAnchor.constraint(equalToConstant: UIScreen.main.bounds.height + heightBuffer) - NSLayoutConstraint.activate([heightConstraints]) - self.heightConstraints = heightConstraints + + let height = UIScreen.main.bounds.height - (safeAreaInsets.top + topInset) + let consts = surfaceView.heightAnchor.constraint(equalToConstant: height) + + NSLayoutConstraint.activate([consts]) + heightConstraints = consts + surfaceView.bottomOverflow = heightBuffer } func activateLayout(of state: FloatingPanelPosition?) { diff --git a/Framework/Sources/FloatingPanelSurfaceView.swift b/Framework/Sources/FloatingPanelSurfaceView.swift index 4a6c6180..42f984c1 100644 --- a/Framework/Sources/FloatingPanelSurfaceView.swift +++ b/Framework/Sources/FloatingPanelSurfaceView.swift @@ -22,6 +22,7 @@ public class FloatingPanelSurfaceView: UIView { public var contentView: UIView! private var color: UIColor? = .white { didSet { setNeedsDisplay() } } + var bottomOverflow: CGFloat = 0.0 { didSet { setNeedsDisplay() }} public override var backgroundColor: UIColor? { get { return color } @@ -73,6 +74,7 @@ public class FloatingPanelSurfaceView: UIView { private func render() { super.backgroundColor = .clear + self.clipsToBounds = false let contentView = FloatingPanelSurfaceContentView() addSubview(contentView) @@ -120,7 +122,9 @@ public class FloatingPanelSurfaceView: UIView { private func makeShadowLayer() -> CAShapeLayer { log.debug("SurfaceView bounds", bounds) let shadowLayer = CAShapeLayer() - let path = UIBezierPath(roundedRect: bounds, + var rect = bounds + rect.size.height += bottomOverflow + let path = UIBezierPath(roundedRect: rect, byRoundingCorners: [.topLeft, .topRight], cornerRadii: CGSize(width: cornerRadius, height: cornerRadius)) shadowLayer.path = path.cgPath From 5bfa747157682852785d12776b1fe13d08e556c1 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Tue, 23 Oct 2018 14:34:49 +0900 Subject: [PATCH 004/623] Escape UIVisualEffectView problem on iOS10 The floating panel controller can't resolve this issue, but the workaround is much easy in the content view controller. So I stop auto-rounding corners in content view on iOS 10. --- Examples/Maps/Maps/Base.lproj/Main.storyboard | 9 ++++--- Examples/Maps/Maps/ViewController.swift | 12 ++++++++- .../Sources/FloatingPanelSurfaceView.swift | 27 ++++++++++++++++--- README.md | 17 ++++++++++++ 4 files changed, 56 insertions(+), 9 deletions(-) diff --git a/Examples/Maps/Maps/Base.lproj/Main.storyboard b/Examples/Maps/Maps/Base.lproj/Main.storyboard index b3913fac..d61cb649 100644 --- a/Examples/Maps/Maps/Base.lproj/Main.storyboard +++ b/Examples/Maps/Maps/Base.lproj/Main.storyboard @@ -60,9 +60,9 @@ - + - + @@ -70,7 +70,7 @@ - + @@ -238,7 +238,7 @@ - + @@ -247,6 +247,7 @@ + diff --git a/Examples/Maps/Maps/ViewController.swift b/Examples/Maps/Maps/ViewController.swift index f6e1d67a..4c6bac56 100644 --- a/Examples/Maps/Maps/ViewController.swift +++ b/Examples/Maps/Maps/ViewController.swift @@ -133,7 +133,8 @@ class SearchPanelViewController: UIViewController, UITableViewDataSource, UITabl @IBOutlet weak var tableView: UITableView! @IBOutlet weak var searchBar: UISearchBar! - + @IBOutlet weak var visualEffectView: UIVisualEffectView! + override func viewDidLoad() { super.viewDidLoad() tableView.dataSource = self @@ -143,6 +144,15 @@ class SearchPanelViewController: UIViewController, UITableViewDataSource, UITabl textField.font = UIFont(name: textField.font!.fontName, size: 15.0) hideHeader() + + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + if #available(iOS 10, *) { + visualEffectView.layer.cornerRadius = 9.0 + visualEffectView.clipsToBounds = true + } } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { diff --git a/Framework/Sources/FloatingPanelSurfaceView.swift b/Framework/Sources/FloatingPanelSurfaceView.swift index 42f984c1..2cae2aa7 100644 --- a/Framework/Sources/FloatingPanelSurfaceView.swift +++ b/Framework/Sources/FloatingPanelSurfaceView.swift @@ -32,7 +32,10 @@ public class FloatingPanelSurfaceView: UIView { } } - /// The radius to use when drawing rounded corners + /// The radius to use when drawing top rounded corners. + /// + /// `self.contentView` is masked with the top rounded corners automatically on iOS 11 and later. + /// On iOS 10, they are not automatically masked because of UIVisualEffectView issue. See https://forums.developer.apple.com/thread/50854 public var cornerRadius: CGFloat = 0.0 { didSet { setNeedsLayout() } } /// A Boolean indicating whether the surface shadow is displayed. @@ -103,10 +106,26 @@ public class FloatingPanelSurfaceView: UIView { public override func layoutSubviews() { super.layoutSubviews() + updateShadowLayer() - // Don't use `contentView.layer.mask` because of UIVisualEffectView issue on ios10, https://forums.developer.apple.com/thread/50854 - contentView.layer.cornerRadius = cornerRadius - contentView.clipsToBounds = true + + if #available(iOS 11, *) { + // Don't use `contentView.clipToBounds` because it makes content view not able to expand the height of a subview of it + // for the bottom overflow like Auto Layout settings of UIVisualEffectView in Main.storyborad of Example/Maps. + // Because the bottom of contentView must be fit to the bottom of a screen to work the `safeLayoutGuide` of a content VC. + let maskLayer = CAShapeLayer() + var rect = bounds + rect.size.height += bottomOverflow + let path = UIBezierPath(roundedRect: rect, + byRoundingCorners: [.topLeft, .topRight], + cornerRadii: CGSize(width: cornerRadius, height: cornerRadius)) + maskLayer.path = path.cgPath + contentView.layer.mask = maskLayer + } else { + // Don't use `contentView.layer.mask` because of UIVisualEffectView issue on ios10, https://forums.developer.apple.com/thread/50854 + // Instead, a user can mask the content view manually in an application. + } + contentView.layer.borderColor = borderColor?.cgColor contentView.layer.borderWidth = borderWidth } diff --git a/README.md b/README.md index 384526b0..a9baee02 100644 --- a/README.md +++ b/README.md @@ -216,6 +216,23 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate { } ``` +## Notes + +### FloatingPanelSurfaceContentView + +* On iOS 10, `FloatingPanelSurfaceContentView.cornerRadius` isn't not automatically masked with the top rounded corners because of UIVisualEffectView issue. See https://forums.developer.apple.com/thread/50854. +So you need to draw top rounding corners of your content. Here is an example in Examples/Maps. +``` +override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + if #available(iOS 10, *) { + visualEffectView.layer.cornerRadius = 9.0 + visualEffectView.clipsToBounds = true + } +} +``` +* If you sets clear color to `FloatingPanelSurfaceContentView.backgrounColor`, please note the bottom overflow of your content on bouncing at full position. To prevent it, you need to expand your content. For example, See Example/Maps's Auto Layout settings of UIVisualEffectView in Main.storyborad. + ## Author Shin Yamamoto From a72ddfb72d0034507c3d44da15a080f7b56f67d7 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Tue, 23 Oct 2018 15:52:07 +0900 Subject: [PATCH 005/623] Fix the initial height of DebugTableViewController --- Examples/Samples/Sources/ViewController.swift | 2 +- Framework/Sources/FloatingPanelController.swift | 1 - Framework/Sources/FloatingPanelLayout.swift | 6 +++++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Examples/Samples/Sources/ViewController.swift b/Examples/Samples/Sources/ViewController.swift index f91277ee..8c3e8bb8 100644 --- a/Examples/Samples/Sources/ViewController.swift +++ b/Examples/Samples/Sources/ViewController.swift @@ -189,7 +189,7 @@ class DebugTableViewController: UITableViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - print("Content View: viewDidAppear") + print("Content View: viewDidAppear", view.bounds) } override func viewWillDisappear(_ animated: Bool) { diff --git a/Framework/Sources/FloatingPanelController.swift b/Framework/Sources/FloatingPanelController.swift index c257f618..bc62d8a9 100644 --- a/Framework/Sources/FloatingPanelController.swift +++ b/Framework/Sources/FloatingPanelController.swift @@ -133,7 +133,6 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI if let parent = parent { self.update(safeAreaInsets: parent.layoutInsets) } - floatingPanel.layoutAdapter.updateHeight() floatingPanel.backdropView.isHidden = (traitCollection.verticalSizeClass == .compact) } diff --git a/Framework/Sources/FloatingPanelLayout.swift b/Framework/Sources/FloatingPanelLayout.swift index aaf3883f..8044e822 100644 --- a/Framework/Sources/FloatingPanelLayout.swift +++ b/Framework/Sources/FloatingPanelLayout.swift @@ -92,7 +92,11 @@ class FloatingPanelLayoutAdapter { var layout: FloatingPanelLayout - var safeAreaInsets: UIEdgeInsets = .zero + var safeAreaInsets: UIEdgeInsets = .zero { + didSet { + updateHeight() + } + } private var heightBuffer: CGFloat = 88.0 // For bounce private var fixedConstraints: [NSLayoutConstraint] = [] From 6bb66e3a303df7f5f2cba1b7b821ecfb4927c8bf Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Wed, 24 Oct 2018 08:24:32 +0900 Subject: [PATCH 006/623] Improve updating the shadow layer of the surface --- Framework/Sources/FloatingPanelSurfaceView.swift | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/Framework/Sources/FloatingPanelSurfaceView.swift b/Framework/Sources/FloatingPanelSurfaceView.swift index 2cae2aa7..2b3495a9 100644 --- a/Framework/Sources/FloatingPanelSurfaceView.swift +++ b/Framework/Sources/FloatingPanelSurfaceView.swift @@ -102,6 +102,10 @@ public class FloatingPanelSurfaceView: UIView { grabberHandle.heightAnchor.constraint(equalToConstant: grabberHandle.frame.height), grabberHandle.centerXAnchor.constraint(equalTo: centerXAnchor), ]) + + let shadowLayer = CAShapeLayer() + layer.insertSublayer(shadowLayer, at: 0) + self.shadowLayer = shadowLayer } public override func layoutSubviews() { @@ -131,18 +135,9 @@ public class FloatingPanelSurfaceView: UIView { } private func updateShadowLayer() { - if shadowLayer != nil { - shadowLayer.removeFromSuperlayer() - } - shadowLayer = makeShadowLayer() - layer.insertSublayer(shadowLayer, at: 0) - } - - private func makeShadowLayer() -> CAShapeLayer { log.debug("SurfaceView bounds", bounds) - let shadowLayer = CAShapeLayer() var rect = bounds - rect.size.height += bottomOverflow + rect.size.height += bottomOverflow // Expand the height for overflow buffer let path = UIBezierPath(roundedRect: rect, byRoundingCorners: [.topLeft, .topRight], cornerRadii: CGSize(width: cornerRadius, height: cornerRadius)) @@ -155,6 +150,5 @@ public class FloatingPanelSurfaceView: UIView { shadowLayer.shadowOpacity = shadowOpacity shadowLayer.shadowRadius = shadowRadius } - return shadowLayer } } From a81b1a05cc8fc772e071c89b3d38557dfe6ee0c7 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Wed, 24 Oct 2018 08:53:24 +0900 Subject: [PATCH 007/623] Update doc comment --- Framework/Sources/FloatingPanelLayout.swift | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Framework/Sources/FloatingPanelLayout.swift b/Framework/Sources/FloatingPanelLayout.swift index 8044e822..84f0e7b5 100644 --- a/Framework/Sources/FloatingPanelLayout.swift +++ b/Framework/Sources/FloatingPanelLayout.swift @@ -8,22 +8,25 @@ import UIKit public protocol FloatingPanelLayout: class { /// Returns the initial position of a floating panel var initialPosition: FloatingPanelPosition { get } + /// Returns an array of FloatingPanelPosition object to tell the applicable position the floating panel controller var supportedPositions: [FloatingPanelPosition] { get } /// Return the interaction buffer of full position. Default is 6.0. var topInteractionBuffer: CGFloat { get } + /// Return the interaction buffer of full position. Default is 6.0. var bottomInteractionBuffer: CGFloat { get } - /// Returns a CGFloat value for a floating panel position(full, half, tip). - /// A value for full position indicates an inset from the safe area top. - /// On the other hand, values fro half and tip positions indicate insets from the safe area bottom. + /// Returns a CGFloat value to determine a floating panel height for each positions(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. func insetFor(position: FloatingPanelPosition) -> CGFloat? - /// Returns layout constraints for a surface view of a floaitng panel. - /// The layout constraints must not include ones for topAnchor and bottomAnchor - /// because constarints for them will be added by the floating panel controller. + + /// Returns X-axis and width layout constraints of the surface view of a floaitng panel. + /// You must not include any Y-axis and height layout constraints of the surface view + /// because their constarints will be configured by the floating panel controller. func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] /// Return the backdrop alpha of black color in full position. Default is 0.3. From fc05a0fa7167856b8b3bdd4c505d5d5f1600ae6d Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Wed, 24 Oct 2018 11:28:52 +0900 Subject: [PATCH 008/623] Add sample codes in Samples app to test a floating panel in TabBar --- .../Sources/Base.lproj/Main.storyboard | 101 ++++++++++++++++- Examples/Samples/Sources/ViewController.swift | 103 +++++++++++++++++- 2 files changed, 199 insertions(+), 5 deletions(-) diff --git a/Examples/Samples/Sources/Base.lproj/Main.storyboard b/Examples/Samples/Sources/Base.lproj/Main.storyboard index 13c9653a..a78ba39b 100644 --- a/Examples/Samples/Sources/Base.lproj/Main.storyboard +++ b/Examples/Samples/Sources/Base.lproj/Main.storyboard @@ -70,13 +70,106 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -114,7 +207,7 @@ - + @@ -169,7 +262,7 @@ - + @@ -241,7 +334,7 @@ Section 1.10.33 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC - + diff --git a/Examples/Samples/Sources/ViewController.swift b/Examples/Samples/Sources/ViewController.swift index 8c3e8bb8..0901b0ac 100644 --- a/Examples/Samples/Sources/ViewController.swift +++ b/Examples/Samples/Sources/ViewController.swift @@ -17,6 +17,7 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable case trackingTextView case showDetail case showModal + case showTabBar var name: String { switch self { @@ -24,6 +25,7 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable case .trackingTextView: return "Scroll tracking (UITextView)" case .showDetail: return "Show Detail Panel" case .showModal: return "Show Modal" + case .showTabBar: return "Show Tab Bar" } } @@ -33,6 +35,7 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable case .trackingTextView: return "ConsoleViewController" case .showDetail: return "DetailViewController" case .showModal: return "ModalViewController" + case .showTabBar: return "TabBarViewController" } } } @@ -120,7 +123,7 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable // Add FloatingPanel to self.view detailPanelVC.addPanel(toParent: self, belowView: nil, animated: true) - case .showModal: + case .showModal, .showTabBar: let modalVC = contentVC present(modalVC, animated: true, completion: nil) default: @@ -273,3 +276,101 @@ class ModalViewController: UIViewController { dismiss(animated: true, completion: nil) } } + +class TabBarViewController: UITabBarController {} + +class TabBarContentViewController: UIViewController, FloatingPanelControllerDelegate { + var fpc: FloatingPanelController! + var consoleVC: DebugTextViewController! + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + // Initialize FloatingPanelController + fpc = FloatingPanelController() + fpc.delegate = self + + // Initialize FloatingPanelController and add the view + fpc.surfaceView.cornerRadius = 6.0 + fpc.surfaceView.shadowHidden = false + + // Add a content view controller and connect with the scroll view + let consoleVC = storyboard?.instantiateViewController(withIdentifier: "ConsoleViewController") as! DebugTextViewController + fpc.show(consoleVC, sender: self) + self.consoleVC = consoleVC + fpc.track(scrollView: consoleVC.textView) + + // Add FloatingPanel to self.view + fpc.addPanel(toParent: self) + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + // Remove FloatingPanel from a view + fpc.removePanelFromParent(animated: false) + } + + func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? { + switch self.tabBarItem.tag { + case 0: + return OneTabBarPanelLayout() + case 1: + return TwoTabBarPanel2Layout() + default: + return nil + } + } + + @IBAction func close(sender: UIButton) { + dismiss(animated: true, completion: nil) + } +} + +extension FloatingPanelLayout { + func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] { + if #available(iOS 11.0, *) { + return [ + surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 0.0), + surfaceView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: 0.0), + ] + } else { + return [ + surfaceView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0.0), + surfaceView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0.0), + ] + } + } +} + +class OneTabBarPanelLayout: FloatingPanelLayout { + var initialPosition: FloatingPanelPosition { + return .tip + } + var supportedPositions: [FloatingPanelPosition] { + return [.full, .tip] + } + + func insetFor(position: FloatingPanelPosition) -> CGFloat? { + switch position { + case .full: return 16.0 + case .tip: return 22.0 + default: return nil + } + } +} + +class TwoTabBarPanel2Layout: FloatingPanelLayout { + var initialPosition: FloatingPanelPosition { + return .half + } + var supportedPositions: [FloatingPanelPosition] { + return [.full, .half] + } + + func insetFor(position: FloatingPanelPosition) -> CGFloat? { + switch position { + case .full: return 16.0 + case .half: return 261 + default: return nil + } + } +} From 1697b12670132618714d77dedf5ab3954fecf0ad Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Wed, 24 Oct 2018 11:38:50 +0900 Subject: [PATCH 009/623] Fix a critical bug on 2(full and half) anchor positions --- Framework/Sources/FloatingPanel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Framework/Sources/FloatingPanel.swift b/Framework/Sources/FloatingPanel.swift index f90a450a..9f56dedd 100644 --- a/Framework/Sources/FloatingPanel.swift +++ b/Framework/Sources/FloatingPanel.swift @@ -370,7 +370,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate switch supportedPositions { case Set([.full, .half]): - return targetPosition(from: [.half, .tip], at: currentY, velocity: velocity) + return targetPosition(from: [.full, .half], at: currentY, velocity: velocity) case Set([.half, .tip]): return targetPosition(from: [.half, .tip], at: currentY, velocity: velocity) case Set([.full, .tip]): From 0e460ecb5c205b985ecc01ad658d30ba6ba00307 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Thu, 25 Oct 2018 09:10:09 +0900 Subject: [PATCH 010/623] Fix the table view height in Examples/Maps The bottom of a scroll view tracked by a floating panel controller must align the bottom of a screen when `FloatingPanelController.contentInsetAdjustmentBehavior` is set to `always`. --- Examples/Maps/Maps/Base.lproj/Main.storyboard | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/Maps/Maps/Base.lproj/Main.storyboard b/Examples/Maps/Maps/Base.lproj/Main.storyboard index d61cb649..f72ea940 100644 --- a/Examples/Maps/Maps/Base.lproj/Main.storyboard +++ b/Examples/Maps/Maps/Base.lproj/Main.storyboard @@ -70,7 +70,7 @@ - + @@ -227,7 +227,7 @@ - + From 730513077de7aac8c339e2b48e268c7ec7cd149c Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Thu, 25 Oct 2018 11:12:51 +0900 Subject: [PATCH 011/623] Update README - Add shields - Add TOC - Update Usage section - Revise the contents --- README.md | 124 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 94 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index a9baee02..b3afc105 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ +[![Version](https://img.shields.io/cocoapods/v/FloatingPanel.svg)](https://cocoapods.org/pods/FloatingPanel) +[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) +[![Platform](https://img.shields.io/cocoapods/p/FloatingPanel.svg)](https://cocoapods.org/pods/FloatingPanel) +[![Swift 4.2](https://img.shields.io/badge/Swift-4.2-orange.svg?style=flat)](https://swift.org/) + # FloatingPanel + FloatingPanel is a simple and easy-to-use UI component for a new interface introduced in Apple Maps, Shortcuts and Stocks app. The new interface displays the related contents and utilities in parallel as a user wants. @@ -8,6 +14,30 @@ The new interface displays the related contents and utilities in parallel as a u ![Maps(Landscape)](https://github.com/SCENEE/FloatingPanel/blob/master/assets/maps-landscape.gif) + + +- [Features](#features) +- [Requirements](#requirements) +- [Installation](#installation) + - [CocoaPods](#cocoapods) + - [Carthage](#carthage) +- [Getting Started](#getting-started) +- [Usage](#usage) + - [Customize the layout of a floating panel with `FloatingPanelLayout` protocol](#customize-the-layout-of-a-floating-panel-with--floatingpanellayout-protocol) + - [Change the initial position, supported positions and height](#change-the-initial-position-supported-positions-and-height) + - [Support your landscape layout](#support-your-landscape-layout) + - [Customize the behavior with `FloatingPanelBehavior` protocol](#customize-the-behavior-with-floatingpanelbehavior-protocol) + - [Modify your floating panel's interaction](#modify-your-floating-panels-interaction) + - [Create an additional floating panel for a detail](#create-an-additional-floating-panel-for-a-detail) + - [Move a positon with an animation](#move-a-positon-with-an-animation) + - [Make your contents correspond with a floating panel behavior](#make-your-contents-correspond-with-a-floating-panel-behavior) +- [Notes](#notes) + - [FloatingPanelSurfaceView's issue on iOS 10](#floatingpanelsurfaceviews-issue-on-ios-10) +- [Author](#author) +- [License](#license) + + + ## Features - [x] Simple container view controller @@ -87,44 +117,39 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate { ## Usage -### Move a positon with an animation +### Customize the layout of a floating panel with `FloatingPanelLayout` protocol -Move a floating panel to the top and middle of a view while opening and closeing a search bar like Apple Maps. +#### Change the initial position, supported positions and height ```swift - func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { - ... - fpc.move(to: .half, animated: true) +class ViewController: UIViewController, FloatingPanelControllerDelegate { + ... + func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? { + return MyFloatingPanelLayout() } + ... +} - func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) { - ... - fpc.move(to: .full, animated: true) +class MyFloatingPanelLayout: FloatingPanelLayout { + public var initialPosition: FloatingPanelPosition { + return .tip } -``` - -### Make your contents correspond with FloatingPanel behavior - -```swift -class ViewController: UIViewController, FloatingPanelControllerDelegate { - ... - func floatingPanelWillBeginDragging(_ vc: FloatingPanelController) { - if vc.position == .full { - searchVC.searchBar.showsCancelButton = false - searchVC.searchBar.resignFirstResponder() - } + public var supportedPositions: [FloatingPanelPosition] { + return [.full, .half, .tip] } - func floatingPanelDidEndDragging(_ vc: FloatingPanelController, withVelocity velocity: CGPoint, targetPosition: FloatingPanelPosition) { - if targetPosition != .full { - searchVC.hideHeader() + public func insetFor(position: FloatingPanelPosition) -> CGFloat? { + switch position { + case .full: return 16.0 # A top inset from safe area + case .half: return 216.0 # A bottom inset from the safe area + case .tip: return 44.0 # A bottom inset from the safe area + default: return nil } } - ... } ``` -### Support your landscape layout with a `FloatingPanelLayout` object +#### Support your landscape layout ```swift class ViewController: UIViewController, FloatingPanelControllerDelegate { @@ -160,7 +185,9 @@ class FloatingPanelLandscapeLayout: FloatingPanelLayout { } ``` -### Modify your floating panel's interaction with a `FloatingPanelBehavior` object +### Customize the behavior with `FloatingPanelBehavior` protocol + +#### Modify your floating panel's interaction ```swift class ViewController: UIViewController, FloatingPanelControllerDelegate { @@ -216,13 +243,50 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate { } ``` +### Move a positon with an animation + +In the following example, I move a floating panel to full or half position while opening or closeing a search bar like Apple Maps. + +```swift + func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { + ... + fpc.move(to: .half, animated: true) + } + + func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) { + ... + fpc.move(to: .full, animated: true) + } +``` + +### Make your contents correspond with a floating panel behavior + +```swift +class ViewController: UIViewController, FloatingPanelControllerDelegate { + ... + func floatingPanelWillBeginDragging(_ vc: FloatingPanelController) { + if vc.position == .full { + searchVC.searchBar.showsCancelButton = false + searchVC.searchBar.resignFirstResponder() + } + } + + func floatingPanelDidEndDragging(_ vc: FloatingPanelController, withVelocity velocity: CGPoint, targetPosition: FloatingPanelPosition) { + if targetPosition != .full { + searchVC.hideHeader() + } + } + ... +} +``` + ## Notes -### FloatingPanelSurfaceContentView +### FloatingPanelSurfaceView's issue on iOS 10 -* On iOS 10, `FloatingPanelSurfaceContentView.cornerRadius` isn't not automatically masked with the top rounded corners because of UIVisualEffectView issue. See https://forums.developer.apple.com/thread/50854. +* On iOS 10, `FloatingPanelSurfaceView.cornerRadius` isn't not automatically masked with the top rounded corners because of UIVisualEffectView issue. See https://forums.developer.apple.com/thread/50854. So you need to draw top rounding corners of your content. Here is an example in Examples/Maps. -``` +```swift override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() if #available(iOS 10, *) { @@ -231,7 +295,7 @@ override func viewDidLayoutSubviews() { } } ``` -* If you sets clear color to `FloatingPanelSurfaceContentView.backgrounColor`, please note the bottom overflow of your content on bouncing at full position. To prevent it, you need to expand your content. For example, See Example/Maps's Auto Layout settings of UIVisualEffectView in Main.storyborad. +* If you sets clear color to `FloatingPanelSurfaceView.backgrounColor`, please note the bottom overflow of your content on bouncing at full position. To prevent it, you need to expand your content. For example, See Example/Maps's Auto Layout settings of UIVisualEffectView in Main.storyborad. ## Author From 607df2c177ab2478518dfd995cf8b7a5de8d919d Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Fri, 26 Oct 2018 14:21:09 +0900 Subject: [PATCH 012/623] Fix FloatingPanelLayout.{topInteractionBuffer,bottomInteractionBuffer} --- Examples/Samples/Sources/ViewController.swift | 5 +++- Examples/Stocks/Stocks/ViewController.swift | 9 ++++--- Framework/Sources/FloatingPanel.swift | 4 +-- Framework/Sources/FloatingPanelLayout.swift | 26 ++++++++++++------- 4 files changed, 28 insertions(+), 16 deletions(-) diff --git a/Examples/Samples/Sources/ViewController.swift b/Examples/Samples/Sources/ViewController.swift index 0901b0ac..3249dff4 100644 --- a/Examples/Samples/Sources/ViewController.swift +++ b/Examples/Samples/Sources/ViewController.swift @@ -365,11 +365,14 @@ class TwoTabBarPanel2Layout: FloatingPanelLayout { var supportedPositions: [FloatingPanelPosition] { return [.full, .half] } + var bottomInteractionBuffer: CGFloat { + return 261.0 - 22.0 + } func insetFor(position: FloatingPanelPosition) -> CGFloat? { switch position { case .full: return 16.0 - case .half: return 261 + case .half: return 261.0 default: return nil } } diff --git a/Examples/Stocks/Stocks/ViewController.swift b/Examples/Stocks/Stocks/ViewController.swift index 6a678091..ea42bc5e 100644 --- a/Examples/Stocks/Stocks/ViewController.swift +++ b/Examples/Stocks/Stocks/ViewController.swift @@ -106,11 +106,14 @@ class FloatingPanelStocksLayout: FloatingPanelLayout { return [.full, .half, .tip] } - public var initialPosition: FloatingPanelPosition { + var initialPosition: FloatingPanelPosition { return .tip } - public func insetFor(position: FloatingPanelPosition) -> CGFloat? { + var topInteractionBuffer: CGFloat { return 0.0 } + var bottomInteractionBuffer: CGFloat { return 0.0 } + + func insetFor(position: FloatingPanelPosition) -> CGFloat? { switch position { case .full: return 56.0 case .half: return 262.0 @@ -118,7 +121,7 @@ class FloatingPanelStocksLayout: FloatingPanelLayout { } } - public func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] { + func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] { return [ surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 0.0), surfaceView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: 0.0), diff --git a/Framework/Sources/FloatingPanel.swift b/Framework/Sources/FloatingPanel.swift index 9f56dedd..f89653e5 100644 --- a/Framework/Sources/FloatingPanel.swift +++ b/Framework/Sources/FloatingPanel.swift @@ -288,9 +288,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate let y = rect.offsetBy(dx: 0.0, dy: dy).origin.y let topY = layoutAdapter.topY - let topInset = layoutAdapter.topInset let topBuffer = layoutAdapter.layout.topInteractionBuffer - let bottomY = layoutAdapter.bottomY let bottomBuffer = layoutAdapter.layout.bottomInteractionBuffer @@ -300,7 +298,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate return max(topY, min(bottomY, y)) } } - return max(topY - topInset + topBuffer, min(bottomY + bottomBuffer, y)) + return max(topY - topBuffer, min(bottomY + bottomBuffer, y)) } private func startAnimation(to targetPosition: FloatingPanelPosition, at distance: CGFloat, with velocity: CGPoint) { diff --git a/Framework/Sources/FloatingPanelLayout.swift b/Framework/Sources/FloatingPanelLayout.swift index 84f0e7b5..24b07fd5 100644 --- a/Framework/Sources/FloatingPanelLayout.swift +++ b/Framework/Sources/FloatingPanelLayout.swift @@ -12,10 +12,10 @@ public protocol FloatingPanelLayout: class { /// Returns an array of FloatingPanelPosition object to tell the applicable position the floating panel controller var supportedPositions: [FloatingPanelPosition] { get } - /// Return the interaction buffer of full position. Default is 6.0. + /// Return the interaction buffer to the top from the top position. Default is 6.0. var topInteractionBuffer: CGFloat { get } - /// Return the interaction buffer of full position. Default is 6.0. + /// 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 positions(full, half and tip). @@ -109,18 +109,22 @@ class FloatingPanelLayoutAdapter { private var offConstraints: [NSLayoutConstraint] = [] private var heightConstraints: NSLayoutConstraint? = nil - var topInset: CGFloat { + private var fullInset: CGFloat { return layout.insetFor(position: .full) ?? 0.0 } - var halfInset: CGFloat { + private var halfInset: CGFloat { return layout.insetFor(position: .half) ?? 0.0 } - var tipInset: CGFloat { + private var tipInset: CGFloat { return layout.insetFor(position: .tip) ?? 0.0 } var topY: CGFloat { - return (safeAreaInsets.top + topInset) + if layout.supportedPositions.contains(.full) { + return (safeAreaInsets.top + fullInset) + } else { + return middleY + } } var middleY: CGFloat { @@ -128,7 +132,11 @@ class FloatingPanelLayoutAdapter { } var bottomY: CGFloat { - return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + tipInset) + if layout.supportedPositions.contains(.tip) { + return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + tipInset) + } else { + return middleY + } } var adjustedContentInsets: UIEdgeInsets { @@ -185,7 +193,7 @@ class FloatingPanelLayoutAdapter { // Flexible surface constarints for full, half, tip and off fullConstraints = [ surfaceView.topAnchor.constraint(equalTo: parent.layoutGuide.topAnchor, - constant: topInset), + constant: fullInset), ] halfConstraints = [ surfaceView.topAnchor.constraint(equalTo: parent.layoutGuide.bottomAnchor, @@ -213,7 +221,7 @@ class FloatingPanelLayoutAdapter { NSLayoutConstraint.deactivate([consts]) } - let height = UIScreen.main.bounds.height - (safeAreaInsets.top + topInset) + let height = UIScreen.main.bounds.height - (safeAreaInsets.top + fullInset) let consts = surfaceView.heightAnchor.constraint(equalToConstant: height) NSLayoutConstraint.activate([consts]) From 6ac07fc4964d9a70a94f5ebec86b8a8a6e664166 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Fri, 19 Oct 2018 17:16:04 +0900 Subject: [PATCH 013/623] Add travis yml --- .travis.yml | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..a02f6e7f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,44 @@ +language: swift +branches: + only: + - master +cache: + directories: + - build + - vendor + - /usr/local/Homebrew + - $HOME/Library/Caches/Homebrew +env: + global: + - LANG=en_US.UTF-8 + - LC_ALL=en_US.UTF-8 +skip_cleanup: true +jobs: + include: + - stage: carthage + osx_image: xcode10 + before_install: + - brew update + - brew outdated carthage || brew upgrade carthage + script: + - carthage build --no-skip-current + + - stage: podspec + osx_image: xcode10 + script: + - pod spec lint + + - stage: check Maps example + osx_image: xcode10 + script: + - xcodebuild -scheme Maps -sdk iphonesimulator clean build + + - stage: check Stocks example + osx_image: xcode10 + script: + - xcodebuild -scheme Stocks -sdk iphonesimulator clean build + + - stage: check Samples example + osx_image: xcode10 + script: + - xcodebuild -scheme Samples -sdk iphonesimulator clean build From 6114f0c74978de62848b50e96712a8b8148f53be Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Fri, 26 Oct 2018 14:56:39 +0900 Subject: [PATCH 014/623] Add 'Build Status' shield in README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b3afc105..70b51f77 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +[![Build Status](https://travis-ci.org/SCENEE/FloatingPanel.svg?branch=master)](https://travis-ci.org/SCENEE/FloatingPanel) [![Version](https://img.shields.io/cocoapods/v/FloatingPanel.svg)](https://cocoapods.org/pods/FloatingPanel) [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) [![Platform](https://img.shields.io/cocoapods/p/FloatingPanel.svg)](https://cocoapods.org/pods/FloatingPanel) From 94c8d22b9f2046a46334db341bc8e3de0a17acc3 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Fri, 26 Oct 2018 18:32:43 +0900 Subject: [PATCH 015/623] Fix Usage contents in README --- README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 70b51f77..e53ff414 100644 --- a/README.md +++ b/README.md @@ -144,9 +144,15 @@ class MyFloatingPanelLayout: FloatingPanelLayout { case .full: return 16.0 # A top inset from safe area case .half: return 216.0 # A bottom inset from the safe area case .tip: return 44.0 # A bottom inset from the safe area - default: return nil } } + + func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] { + return [ + surfaceView.leftAnchor.constraint(equalTo: view.sideLayoutGuide.leftAnchor, constant: 0.0), + surfaceView.rightAnchor.constraint(equalTo: view.sideLayoutGuide.rightAnchor, constant: 0.0), + ] + } } ``` @@ -156,7 +162,7 @@ class MyFloatingPanelLayout: FloatingPanelLayout { class ViewController: UIViewController, FloatingPanelControllerDelegate { ... func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? { - return (newCollection.verticalSizeClass == .compact) ? FloatingPanelLandscapeLayout() : nil + return (newCollection.verticalSizeClass == .compact) ? FloatingPanelLandscapeLayout() : nil // Returing nil indicates to use the default layout } ... } From 5a7a2a4f4e5e64b45ec1b02342bca2e2b9b99859 Mon Sep 17 00:00:00 2001 From: Ortwin Gentz Date: Fri, 26 Oct 2018 15:30:11 +0200 Subject: [PATCH 016/623] Fixed some typos and language in comments --- Framework/Sources/FloatingPanel.swift | 2 +- Framework/Sources/FloatingPanelBehavior.swift | 2 +- .../Sources/FloatingPanelController.swift | 22 +++++++++---------- Framework/Sources/FloatingPanelLayout.swift | 8 +++---- .../Sources/FloatingPanelSurfaceView.swift | 6 ++--- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/Framework/Sources/FloatingPanel.swift b/Framework/Sources/FloatingPanel.swift index f89653e5..6e08a463 100644 --- a/Framework/Sources/FloatingPanel.swift +++ b/Framework/Sources/FloatingPanel.swift @@ -225,7 +225,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate private func panningBegan() { // A user interaction does not always start from Began state of the pan gesture // because it can be recognized in scrolling a content in a content view controller. - // So I don't nothing here. + // So I do nothing here. log.debug("panningBegan \(initialFrame)") } diff --git a/Framework/Sources/FloatingPanelBehavior.swift b/Framework/Sources/FloatingPanelBehavior.swift index 6c37dd0c..1d279fa9 100644 --- a/Framework/Sources/FloatingPanelBehavior.swift +++ b/Framework/Sources/FloatingPanelBehavior.swift @@ -6,7 +6,7 @@ import UIKit public protocol FloatingPanelBehavior { - // Returns a UIViewPropertyAnimator object in interacting a floating panel by a user pan gesture + // Returns a UIViewPropertyAnimator object for interacting with a floating panel by a user pan gesture func interactionAnimator(_ fpc: FloatingPanelController, to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator // Returns a UIViewPropertyAnimator object to present a floating panel diff --git a/Framework/Sources/FloatingPanelController.swift b/Framework/Sources/FloatingPanelController.swift index bc62d8a9..c053144b 100644 --- a/Framework/Sources/FloatingPanelController.swift +++ b/Framework/Sources/FloatingPanelController.swift @@ -66,7 +66,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI return floatingPanel.backdropView } - /// Returns the scroll view that the conroller tracks. + /// Returns the scroll view that the controller tracks. public weak var scrollView: UIScrollView? { return floatingPanel.scrollView } @@ -76,14 +76,14 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI return floatingPanel.state } - /// The content insets of the tracking scroll view derived the safe area of the parent view + /// The content insets of the tracking scroll view derived from the safe area of the parent view public var adjustedContentInsets: UIEdgeInsets { return floatingPanel.layoutAdapter.adjustedContentInsets } /// The behavior for determining the adjusted content offsets. /// - /// This property specifies how the content area of the tracking scroll view are modified using `adjustedContentInsets`. The default value of this property is FloatingPanelController.ContentInsetAdjustmentBehavior.always. + /// This property specifies how the content area of the tracking scroll view is modified using `adjustedContentInsets`. The default value of this property is FloatingPanelController.ContentInsetAdjustmentBehavior.always. public var contentInsetAdjustmentBehavior: ContentInsetAdjustmentBehavior = .always private var floatingPanel: FloatingPanel! @@ -92,7 +92,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI super.init(coder: aDecoder) } - /// Initialize a newly created a floating panel controller. + /// Initialize a newly created floating panel controller. public init() { super.init(nibName: nil, bundle: nil) } @@ -139,7 +139,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI public override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - // I needs to update safeAreaInsets here to ensure that the `adjustedContentInsets` has a correct value. + // Need to update safeAreaInsets here to ensure that the `adjustedContentInsets` has a correct value. // Because the parent VC does not call viewSafeAreaInsetsDidChange() expectedly and // `view.safeAreaInsets` has a correct value of the bottom inset here. if let parent = parent { @@ -173,10 +173,10 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI // MARK: - Container view controller interface - /// Adds the view managed the controller as a child of the specified view controller. + /// Adds the view managed by the controller as a child of the specified view controller. /// - Parameters: - /// - parent: A parent view controller object that displays FloatingPanelController's view. A conatiner view controller object isn't applicable. - /// - belowView: Insert the surface view managed by the controller below the specified view. As default, the surface view will be added to the end of the parent list of subviews. + /// - parent: A parent view controller object that displays FloatingPanelController's view. A container view controller object isn't applicable. + /// - belowView: Insert the surface view managed by the controller below the specified view. By default, the surface view will be added to the end of the parent list of subviews. /// - animated: Pass true to animate the presentation; otherwise, pass false. public func addPanel(toParent parent: UIViewController, belowView: UIView? = nil, animated: Bool = false) { guard self.parent == nil else { @@ -196,7 +196,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI parent.addChild(self) - // Must set a layout again here because `self.traitCollection` is applied correctly on it's added to a parent VC + // 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.layoutViews(in: parent) floatingPanel.present(animated: animated) { [weak self] in @@ -229,7 +229,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI /// - Parameters: /// - to: Pass a FloatingPanelPosition value to move the surface view to the position. /// - animated: Pass true to animate the presentation; otherwise, pass false. - /// - completion: The block to execute after the view controller is dismissed. This block has no return value and takes no parameters. You may specify nil for this parameter. + /// - 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) { floatingPanel.move(to: to, animated: animated, completion: completion) } @@ -255,7 +255,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI /// Tracks the specified scroll view to correspond with the scroll. /// /// - Attention: - /// The specified scroll view must be already assigned the delegate property because the controller intemediates the several delegate methods. + /// The specified scroll view must be already assigned to the delegate property because the controller intermediates between the various delegate methods. /// public func track(scrollView: UIScrollView) { floatingPanel.scrollView = scrollView diff --git a/Framework/Sources/FloatingPanelLayout.swift b/Framework/Sources/FloatingPanelLayout.swift index 24b07fd5..e7b5ff3a 100644 --- a/Framework/Sources/FloatingPanelLayout.swift +++ b/Framework/Sources/FloatingPanelLayout.swift @@ -9,7 +9,7 @@ public protocol FloatingPanelLayout: class { /// Returns the initial position of a floating panel var initialPosition: FloatingPanelPosition { get } - /// Returns an array of FloatingPanelPosition object to tell the applicable position the floating panel controller + /// Returns an array of FloatingPanelPosition objects to tell the applicable positions of the floating panel controller var supportedPositions: [FloatingPanelPosition] { get } /// Return the interaction buffer to the top from the top position. Default is 6.0. @@ -18,15 +18,15 @@ 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 positions(full, half and tip). + /// 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. func insetFor(position: FloatingPanelPosition) -> CGFloat? - /// Returns X-axis and width layout constraints of the surface view of a floaitng panel. + /// Returns X-axis and width layout constraints of the surface view of a floating panel. /// You must not include any Y-axis and height layout constraints of the surface view - /// because their constarints will be configured by the floating panel controller. + /// because their constraints will be configured by the floating panel controller. func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] /// Return the backdrop alpha of black color in full position. Default is 0.3. diff --git a/Framework/Sources/FloatingPanelSurfaceView.swift b/Framework/Sources/FloatingPanelSurfaceView.swift index 2b3495a9..ffb5589d 100644 --- a/Framework/Sources/FloatingPanelSurfaceView.swift +++ b/Framework/Sources/FloatingPanelSurfaceView.swift @@ -35,7 +35,7 @@ public class FloatingPanelSurfaceView: UIView { /// The radius to use when drawing top rounded corners. /// /// `self.contentView` is masked with the top rounded corners automatically on iOS 11 and later. - /// On iOS 10, they are not automatically masked because of UIVisualEffectView issue. See https://forums.developer.apple.com/thread/50854 + /// On iOS 10, they are not automatically masked because of a UIVisualEffectView issue. See https://forums.developer.apple.com/thread/50854 public var cornerRadius: CGFloat = 0.0 { didSet { setNeedsLayout() } } /// A Boolean indicating whether the surface shadow is displayed. @@ -114,7 +114,7 @@ public class FloatingPanelSurfaceView: UIView { updateShadowLayer() if #available(iOS 11, *) { - // Don't use `contentView.clipToBounds` because it makes content view not able to expand the height of a subview of it + // Don't use `contentView.clipToBounds` because it prevents content view from expanding the height of a subview of it // for the bottom overflow like Auto Layout settings of UIVisualEffectView in Main.storyborad of Example/Maps. // Because the bottom of contentView must be fit to the bottom of a screen to work the `safeLayoutGuide` of a content VC. let maskLayer = CAShapeLayer() @@ -126,7 +126,7 @@ public class FloatingPanelSurfaceView: UIView { maskLayer.path = path.cgPath contentView.layer.mask = maskLayer } else { - // Don't use `contentView.layer.mask` because of UIVisualEffectView issue on ios10, https://forums.developer.apple.com/thread/50854 + // Don't use `contentView.layer.mask` because of a UIVisualEffectView issue in iOS 10, https://forums.developer.apple.com/thread/50854 // Instead, a user can mask the content view manually in an application. } From 64083ac1c63619f28ada957b655f4140e955c247 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Sat, 27 Oct 2018 14:51:00 +0900 Subject: [PATCH 017/623] Fix untracked scroll view's freezing in a floating panel --- Framework/Sources/FloatingPanel.swift | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Framework/Sources/FloatingPanel.swift b/Framework/Sources/FloatingPanel.swift index 6e08a463..930ed441 100644 --- a/Framework/Sources/FloatingPanel.swift +++ b/Framework/Sources/FloatingPanel.swift @@ -166,7 +166,18 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { guard gestureRecognizer == panGesture else { return false } - // Do not begin any gestures excluding scrollView?.panGestureRecognizer until the pan gesture fails + // Do not begin any gestures excluding the tracking scrollView's pan gesture until the pan gesture fails + if otherGestureRecognizer == scrollView?.panGestureRecognizer { + return false + } else { + return true + } + } + + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool { + guard gestureRecognizer == panGesture else { return false } + + // Do not begin the pan gesture until any other gestures fail except fo the tracking scrollView's pan gesture. if otherGestureRecognizer == scrollView?.panGestureRecognizer { return false } else { From d393eb3658322cefd06ff342472ed7604093382d Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Sat, 27 Oct 2018 15:16:48 +0900 Subject: [PATCH 018/623] Add a nested scroll view's sample --- .../Sources/Base.lproj/Main.storyboard | 142 ++++++++++++++++++ Examples/Samples/Sources/ViewController.swift | 32 +++- 2 files changed, 172 insertions(+), 2 deletions(-) diff --git a/Examples/Samples/Sources/Base.lproj/Main.storyboard b/Examples/Samples/Sources/Base.lproj/Main.storyboard index a78ba39b..d63a963c 100644 --- a/Examples/Samples/Sources/Base.lproj/Main.storyboard +++ b/Examples/Samples/Sources/Base.lproj/Main.storyboard @@ -209,6 +209,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -243,6 +364,7 @@ + @@ -254,6 +376,11 @@ + + + + + @@ -261,6 +388,21 @@ + + + + + + + + + + + + + + + diff --git a/Examples/Samples/Sources/ViewController.swift b/Examples/Samples/Sources/ViewController.swift index 3249dff4..109673ff 100644 --- a/Examples/Samples/Sources/ViewController.swift +++ b/Examples/Samples/Sources/ViewController.swift @@ -18,6 +18,7 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable case showDetail case showModal case showTabBar + case showNestedScrollView var name: String { switch self { @@ -26,6 +27,7 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable case .showDetail: return "Show Detail Panel" case .showModal: return "Show Modal" case .showTabBar: return "Show Tab Bar" + case .showNestedScrollView: return "Show Nested ScrollView" } } @@ -36,6 +38,7 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable case .showDetail: return "DetailViewController" case .showModal: return "ModalViewController" case .showTabBar: return "TabBarViewController" + case .showNestedScrollView: return "NestedScrollViewController" } } } @@ -70,9 +73,10 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable case let contentVC as DebugTableViewController: mainPanelVC.track(scrollView: contentVC.tableView) - + case let contentVC as NestedScrollViewController: + mainPanelVC.track(scrollView: contentVC.scrollView) default: - fatalError() + break } // Add FloatingPanel to self.view mainPanelVC.addPanel(toParent: self, belowView: nil, animated: true) @@ -135,12 +139,27 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable } } +class NestedScrollViewController: UIViewController { + @IBOutlet weak var scrollView: UIScrollView! + + @IBAction func longPressed(_ sender: Any) { + print("LongPressed!") + } + @IBAction func swipped(_ sender: Any) { + print("Swipped!") + } + @IBAction func tapped(_ sender: Any) { + print("Tapped!") + } +} + class DebugTextViewController: UIViewController, UITextViewDelegate { @IBOutlet weak var textView: UITextView! override func viewDidLoad() { super.viewDidLoad() textView.delegate = self + if #available(iOS 11.0, *) { textView.contentInsetAdjustmentBehavior = .never } @@ -241,6 +260,15 @@ class DetailViewController: UIViewController { // dismiss(animated: true, completion: nil) (self.parent as? FloatingPanelController)?.removePanelFromParent(animated: true, completion: nil) } + @IBAction func tapped(_ sender: Any) { + print("Detail panel is tapped!") + } + @IBAction func swipped(_ sender: Any) { + print("Detail panel is swipped!") + } + @IBAction func longPressed(_ sender: Any) { + print("Detail panel is longPressed!") + } } class ModalViewController: UIViewController { From 635af96fc80a0e1462d20be420e94ad2be2cd143 Mon Sep 17 00:00:00 2001 From: 0xflotus <26602940+0xflotus@users.noreply.github.com> Date: Sat, 27 Oct 2018 21:23:06 +0200 Subject: [PATCH 019/623] fixed some errors --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index e53ff414..7418203d 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ The new interface displays the related contents and utilities in parallel as a u - [Customize the behavior with `FloatingPanelBehavior` protocol](#customize-the-behavior-with-floatingpanelbehavior-protocol) - [Modify your floating panel's interaction](#modify-your-floating-panels-interaction) - [Create an additional floating panel for a detail](#create-an-additional-floating-panel-for-a-detail) - - [Move a positon with an animation](#move-a-positon-with-an-animation) + - [Move a position with an animation](#move-a-position-with-an-animation) - [Make your contents correspond with a floating panel behavior](#make-your-contents-correspond-with-a-floating-panel-behavior) - [Notes](#notes) - [FloatingPanelSurfaceView's issue on iOS 10](#floatingpanelsurfaceviews-issue-on-ios-10) @@ -162,7 +162,7 @@ class MyFloatingPanelLayout: FloatingPanelLayout { class ViewController: UIViewController, FloatingPanelControllerDelegate { ... func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? { - return (newCollection.verticalSizeClass == .compact) ? FloatingPanelLandscapeLayout() : nil // Returing nil indicates to use the default layout + return (newCollection.verticalSizeClass == .compact) ? FloatingPanelLandscapeLayout() : nil // Returning nil indicates to use the default layout } ... } @@ -250,9 +250,9 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate { } ``` -### Move a positon with an animation +### Move a position with an animation -In the following example, I move a floating panel to full or half position while opening or closeing a search bar like Apple Maps. +In the following example, I move a floating panel to full or half position while opening or closing a search bar like Apple Maps. ```swift func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { @@ -302,7 +302,7 @@ override func viewDidLayoutSubviews() { } } ``` -* If you sets clear color to `FloatingPanelSurfaceView.backgrounColor`, please note the bottom overflow of your content on bouncing at full position. To prevent it, you need to expand your content. For example, See Example/Maps's Auto Layout settings of UIVisualEffectView in Main.storyborad. +* If you sets clear color to `FloatingPanelSurfaceView.backgroundColor`, please note the bottom overflow of your content on bouncing at full position. To prevent it, you need to expand your content. For example, See Example/Maps's Auto Layout settings of UIVisualEffectView in Main.storyborad. ## Author From 2866ff08e4e608c6131692139d0e88dbafbba7d4 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Sat, 27 Oct 2018 10:06:43 +0900 Subject: [PATCH 020/623] Fix README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7418203d..96ab435c 100644 --- a/README.md +++ b/README.md @@ -141,9 +141,9 @@ class MyFloatingPanelLayout: FloatingPanelLayout { public func insetFor(position: FloatingPanelPosition) -> CGFloat? { switch position { - case .full: return 16.0 # A top inset from safe area - case .half: return 216.0 # A bottom inset from the safe area - case .tip: return 44.0 # A bottom inset from the safe area + case .full: return 16.0 // A top inset from safe area + case .half: return 216.0 // A bottom inset from the safe area + case .tip: return 44.0 // A bottom inset from the safe area } } From 0d7350911830ad7a0d28b33e4a065f7237c3fcae Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Fri, 26 Oct 2018 08:41:31 +0900 Subject: [PATCH 021/623] Improve the default impls of FloatingPanelLayout methods --- Examples/Stocks/Stocks/ViewController.swift | 11 ---------- Framework/Sources/FloatingPanelLayout.swift | 23 +++++++++++---------- README.md | 12 +---------- 3 files changed, 13 insertions(+), 33 deletions(-) diff --git a/Examples/Stocks/Stocks/ViewController.swift b/Examples/Stocks/Stocks/ViewController.swift index ea42bc5e..91755bc2 100644 --- a/Examples/Stocks/Stocks/ViewController.swift +++ b/Examples/Stocks/Stocks/ViewController.swift @@ -102,10 +102,6 @@ class NewsViewController: UIViewController { // MARK: My custom layout class FloatingPanelStocksLayout: FloatingPanelLayout { - public var supportedPositions: [FloatingPanelPosition] { - return [.full, .half, .tip] - } - var initialPosition: FloatingPanelPosition { return .tip } @@ -121,13 +117,6 @@ class FloatingPanelStocksLayout: FloatingPanelLayout { } } - func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] { - return [ - surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 0.0), - surfaceView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: 0.0), - ] - } - var backdropAlpha: CGFloat = 0.0 } diff --git a/Framework/Sources/FloatingPanelLayout.swift b/Framework/Sources/FloatingPanelLayout.swift index e7b5ff3a..413f7df5 100644 --- a/Framework/Sources/FloatingPanelLayout.swift +++ b/Framework/Sources/FloatingPanelLayout.swift @@ -6,10 +6,10 @@ import UIKit public protocol FloatingPanelLayout: class { - /// Returns the initial position of a floating panel + /// Returns the initial position of a floating panel. var initialPosition: FloatingPanelPosition { get } - /// Returns an array of FloatingPanelPosition objects to tell the applicable positions of the floating panel controller + /// Returns an array of FloatingPanelPosition objects to tell the applicable positions of the floating panel controller. Default is all of them. var supportedPositions: [FloatingPanelPosition] { get } /// Return the interaction buffer to the top from the top position. Default is 6.0. @@ -27,6 +27,7 @@ public protocol FloatingPanelLayout: class { /// Returns X-axis and width layout constraints of the surface view of a floating panel. /// You must not include any Y-axis and height layout constraints of the surface view /// because their constraints will be configured by the floating panel controller. + /// By default, the width of a surface view fits a safe area. func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] /// Return the backdrop alpha of black color in full position. Default is 0.3. @@ -37,13 +38,20 @@ public extension FloatingPanelLayout { var backdropAlpha: CGFloat { return 0.3 } var topInteractionBuffer: CGFloat { return 6.0 } var bottomInteractionBuffer: CGFloat { return 6.0 } -} -public class FloatingPanelDefaultLayout: FloatingPanelLayout { public var supportedPositions: [FloatingPanelPosition] { return [.full, .half, .tip] } + + func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] { + return [ + surfaceView.leftAnchor.constraint(equalTo: view.sideLayoutGuide.leftAnchor, constant: 0.0), + surfaceView.rightAnchor.constraint(equalTo: view.sideLayoutGuide.rightAnchor, constant: 0.0), + ] + } +} +public class FloatingPanelDefaultLayout: FloatingPanelLayout { public var initialPosition: FloatingPanelPosition { return .half } @@ -55,13 +63,6 @@ public class FloatingPanelDefaultLayout: FloatingPanelLayout { case .tip: return 69.0 } } - - public func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] { - return [ - surfaceView.leftAnchor.constraint(equalTo: view.sideLayoutGuide.leftAnchor, constant: 0.0), - surfaceView.rightAnchor.constraint(equalTo: view.sideLayoutGuide.rightAnchor, constant: 0.0), - ] - } } public class FloatingPanelDefaultLandscapeLayout: FloatingPanelLayout { diff --git a/README.md b/README.md index 96ab435c..f4d6d552 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,7 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate { ### Customize the layout of a floating panel with `FloatingPanelLayout` protocol -#### Change the initial position, supported positions and height +#### Change the initial position and height ```swift class ViewController: UIViewController, FloatingPanelControllerDelegate { @@ -135,9 +135,6 @@ class MyFloatingPanelLayout: FloatingPanelLayout { public var initialPosition: FloatingPanelPosition { return .tip } - public var supportedPositions: [FloatingPanelPosition] { - return [.full, .half, .tip] - } public func insetFor(position: FloatingPanelPosition) -> CGFloat? { switch position { @@ -146,13 +143,6 @@ class MyFloatingPanelLayout: FloatingPanelLayout { case .tip: return 44.0 // A bottom inset from the safe area } } - - func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] { - return [ - surfaceView.leftAnchor.constraint(equalTo: view.sideLayoutGuide.leftAnchor, constant: 0.0), - surfaceView.rightAnchor.constraint(equalTo: view.sideLayoutGuide.rightAnchor, constant: 0.0), - ] - } } ``` From db06a8d075978ddec1baecc6fbbeaaced73aed28 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Sat, 27 Oct 2018 10:21:44 +0900 Subject: [PATCH 022/623] Change the type of 'supportedPositions' from Array to Set --- Examples/Samples/Sources/ViewController.swift | 4 ++-- Framework/Sources/FloatingPanel.swift | 8 ++++---- Framework/Sources/FloatingPanelController.swift | 2 +- Framework/Sources/FloatingPanelLayout.swift | 10 +++++----- README.md | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Examples/Samples/Sources/ViewController.swift b/Examples/Samples/Sources/ViewController.swift index 109673ff..0f17c698 100644 --- a/Examples/Samples/Sources/ViewController.swift +++ b/Examples/Samples/Sources/ViewController.swift @@ -373,7 +373,7 @@ class OneTabBarPanelLayout: FloatingPanelLayout { var initialPosition: FloatingPanelPosition { return .tip } - var supportedPositions: [FloatingPanelPosition] { + var supportedPositions: Set { return [.full, .tip] } @@ -390,7 +390,7 @@ class TwoTabBarPanel2Layout: FloatingPanelLayout { var initialPosition: FloatingPanelPosition { return .half } - var supportedPositions: [FloatingPanelPosition] { + var supportedPositions: Set { return [.full, .half] } var bottomInteractionBuffer: CGFloat { diff --git a/Framework/Sources/FloatingPanel.swift b/Framework/Sources/FloatingPanel.swift index 930ed441..83e06fac 100644 --- a/Framework/Sources/FloatingPanel.swift +++ b/Framework/Sources/FloatingPanel.swift @@ -373,16 +373,16 @@ 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: Set = layoutAdapter.layout.supportedPositions assert(supportedPositions.count > 1) switch supportedPositions { - case Set([.full, .half]): + case [.full, .half]: return targetPosition(from: [.full, .half], at: currentY, velocity: velocity) - case Set([.half, .tip]): + case [.half, .tip]: return targetPosition(from: [.half, .tip], at: currentY, velocity: velocity) - case Set([.full, .tip]): + case [.full, .tip]: return targetPosition(from: [.full, .tip], at: currentY, velocity: velocity) default: /* diff --git a/Framework/Sources/FloatingPanelController.swift b/Framework/Sources/FloatingPanelController.swift index c053144b..8d1e1ad1 100644 --- a/Framework/Sources/FloatingPanelController.swift +++ b/Framework/Sources/FloatingPanelController.swift @@ -36,7 +36,7 @@ public extension FloatingPanelControllerDelegate { func floatingPanelDidEndDecelerating(_ vc: FloatingPanelController) {} } -public enum FloatingPanelPosition: Int { +public enum FloatingPanelPosition: Int, CaseIterable { case full case half case tip diff --git a/Framework/Sources/FloatingPanelLayout.swift b/Framework/Sources/FloatingPanelLayout.swift index 413f7df5..4b317e0c 100644 --- a/Framework/Sources/FloatingPanelLayout.swift +++ b/Framework/Sources/FloatingPanelLayout.swift @@ -9,8 +9,8 @@ public protocol FloatingPanelLayout: class { /// Returns the initial position of a floating panel. var initialPosition: FloatingPanelPosition { get } - /// Returns an array of FloatingPanelPosition objects to tell the applicable positions of the floating panel controller. Default is all of them. - var supportedPositions: [FloatingPanelPosition] { get } + /// Returns a set of FloatingPanelPosition objects to tell the applicable positions of the floating panel controller. Default is all of them. + var supportedPositions: Set { get } /// Return the interaction buffer to the top from the top position. Default is 6.0. var topInteractionBuffer: CGFloat { get } @@ -39,8 +39,8 @@ public extension FloatingPanelLayout { var topInteractionBuffer: CGFloat { return 6.0 } var bottomInteractionBuffer: CGFloat { return 6.0 } - public var supportedPositions: [FloatingPanelPosition] { - return [.full, .half, .tip] + public var supportedPositions: Set { + return Set(FloatingPanelPosition.allCases) } func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] { @@ -69,7 +69,7 @@ public class FloatingPanelDefaultLandscapeLayout: FloatingPanelLayout { public var initialPosition: FloatingPanelPosition { return .tip } - public var supportedPositions: [FloatingPanelPosition] { + public var supportedPositions: Set { return [.full, .tip] } diff --git a/README.md b/README.md index f4d6d552..5aba6462 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,7 @@ class FloatingPanelLandscapeLayout: FloatingPanelLayout { public var initialPosition: FloatingPanelPosition { return .tip } - public var supportedPositions: [FloatingPanelPosition] { + public var supportedPositions: Set { return [.full, .tip] } From c8d93e06f8c2421dd9be411beb3848fb2b3f6f63 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Sat, 27 Oct 2018 10:58:12 +0900 Subject: [PATCH 023/623] Check consistance of FloatingPanelLayout --- Framework/Sources/FloatingPanelLayout.swift | 29 +++++++++++++++------ 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/Framework/Sources/FloatingPanelLayout.swift b/Framework/Sources/FloatingPanelLayout.swift index 4b317e0c..926573ba 100644 --- a/Framework/Sources/FloatingPanelLayout.swift +++ b/Framework/Sources/FloatingPanelLayout.swift @@ -94,7 +94,9 @@ class FloatingPanelLayoutAdapter { private weak var surfaceView: FloatingPanelSurfaceView! private weak var backdropVIew: FloatingPanelBackdropView! - var layout: FloatingPanelLayout + var layout: FloatingPanelLayout { + didSet { checkConsistance(of: layout) } + } var safeAreaInsets: UIEdgeInsets = .zero { didSet { @@ -162,13 +164,6 @@ class FloatingPanelLayoutAdapter { self.layout = layout self.surfaceView = surfaceView self.backdropVIew = backdropView - - // Verify layout configurations - assert(layout.supportedPositions.count > 1) - assert(layout.supportedPositions.contains(layout.initialPosition)) - if halfInset > 0 { - assert(halfInset >= tipInset) - } } func prepareLayout(toParent parent: UIViewController) { @@ -260,4 +255,22 @@ class FloatingPanelLayoutAdapter { NSLayoutConstraint.activate(tipConstraints) } } + + private func checkConsistance(of layout: FloatingPanelLayout) { + // Verify layout configurations + assert(layout.supportedPositions.count > 1) + assert(layout.supportedPositions.contains(layout.initialPosition), + "Does not include an initial potision(\(layout.initialPosition)) in supportedPositions(\(layout.supportedPositions))") + layout.supportedPositions.forEach { (pos) in + assert(layout.insetFor(position: pos) != nil, + "Undefined an inset for a pos(\(pos))") + } + if halfInset > 0 { + assert(halfInset > tipInset, "Invalid half and tip insets") + } + if fullInset > 0 { + assert(middleY > topY, "Invalid insets") + assert(bottomY > topY, "Invalid insets") + } + } } From 58cd261638f3ab1760fdee0c843bc54860255086 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Sat, 27 Oct 2018 11:23:04 +0900 Subject: [PATCH 024/623] Open the pan gesture recognizer of FloatingPanelController --- Framework/Sources/FloatingPanel.swift | 23 +++++++++++++++++-- .../Sources/FloatingPanelController.swift | 19 ++++++++++----- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/Framework/Sources/FloatingPanel.swift b/Framework/Sources/FloatingPanel.swift index 83e06fac..e76b2f77 100644 --- a/Framework/Sources/FloatingPanel.swift +++ b/Framework/Sources/FloatingPanel.swift @@ -35,8 +35,9 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate private(set) var state: FloatingPanelPosition = .tip + let panGesture: FloatingPanelPanGestureRecognizer + private var animator: UIViewPropertyAnimator? - private let panGesture: UIPanGestureRecognizer private var initialFrame: CGRect = .zero private var transOffsetY: CGFloat = 0 private var interactionInProgress: Bool = false @@ -60,7 +61,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate layout: layout) self.behavior = behavior - panGesture = UIPanGestureRecognizer() + panGesture = FloatingPanelPanGestureRecognizer() if #available(iOS 11.0, *) { panGesture.name = "FloatingPanelSurface" @@ -494,3 +495,21 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate } } } + +class FloatingPanelPanGestureRecognizer: UIPanGestureRecognizer { + override weak var delegate: UIGestureRecognizerDelegate? { + get { + return super.delegate + } + set { + guard newValue is FloatingPanel else { + let exception = NSException(name: .invalidArgumentException, + reason: "FloatingPanelController's built-in pan gesture recognizer must have its controller as its delegate.", + userInfo: nil) + exception.raise() + return + } + super.delegate = newValue + } + } +} diff --git a/Framework/Sources/FloatingPanelController.swift b/Framework/Sources/FloatingPanelController.swift index 8d1e1ad1..cc06700b 100644 --- a/Framework/Sources/FloatingPanelController.swift +++ b/Framework/Sources/FloatingPanelController.swift @@ -71,6 +71,11 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI return floatingPanel.scrollView } + // The underlying gesture recognizer for pan gestures + public var panGestureRecognizer: UIPanGestureRecognizer { + return floatingPanel.panGesture + } + /// The current position of the floating panel controller's contents. public var position: FloatingPanelPosition { return floatingPanel.state @@ -90,11 +95,19 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) + + floatingPanel = FloatingPanel(self, + layout: fetchLayout(for: self.traitCollection), + behavior: fetchBehavior(for: self.traitCollection)) } /// Initialize a newly created floating panel controller. public init() { super.init(nibName: nil, bundle: nil) + + floatingPanel = FloatingPanel(self, + layout: fetchLayout(for: self.traitCollection), + behavior: fetchBehavior(for: self.traitCollection)) } /// Creates the view that the controller manages. @@ -105,12 +118,6 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI view.backgroundColor = .white self.view = view as UIView - - let layout = fetchLayout(for: self.traitCollection) - let behavior = fetchBehavior(for: self.traitCollection) - floatingPanel = FloatingPanel(self, - layout: layout, - behavior: behavior) } public override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) { From 9c1d7ca372d9ee4bfcea06f8703e2256a43fde56 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Sat, 27 Oct 2018 14:21:33 +0900 Subject: [PATCH 025/623] Improve FloatingPanelBehavior protocol present/dismiss words should be used for modality. add/remove words are appropriate for them. --- Framework/Sources/FloatingPanel.swift | 55 ++++++++++--------- Framework/Sources/FloatingPanelBehavior.swift | 31 ++++++++--- 2 files changed, 54 insertions(+), 32 deletions(-) diff --git a/Framework/Sources/FloatingPanel.swift b/Framework/Sources/FloatingPanel.swift index e76b2f77..767b3619 100644 --- a/Framework/Sources/FloatingPanel.swift +++ b/Framework/Sources/FloatingPanel.swift @@ -84,48 +84,53 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate } func move(to: FloatingPanelPosition, animated: Bool, completion: (() -> Void)? = nil) { - if to != .full { - lockScrollView() - } - - if animated { - let animator = behavior.presentAnimator(self.viewcontroller, from: state, to: to) - animator.addAnimations { [weak self] in - guard let self = self else { return } - - self.updateLayout(to: to) - self.state = to - } - animator.addCompletion { _ in - completion?() - } - animator.startAnimation() - } else { - self.updateLayout(to: to) - self.state = to - completion?() - } + move(from: state, to: to, animated: animated, completion: completion) } func present(animated: Bool, completion: (() -> Void)? = nil) { self.layoutAdapter.activateLayout(of: nil) - move(to: layoutAdapter.layout.initialPosition, animated: animated, completion: completion) + move(from: nil, to: layoutAdapter.layout.initialPosition, animated: animated, completion: completion) } func dismiss(animated: Bool, completion: (() -> Void)? = nil) { + move(from: state, to: nil, animated: animated, completion: completion) + } + + private func move(from: FloatingPanelPosition?, to: FloatingPanelPosition?, animated: Bool, completion: (() -> Void)? = nil) { + if to != .full { + lockScrollView() + } + if animated { - let animator = behavior.dismissAnimator(self.viewcontroller, from: state) + let animator: UIViewPropertyAnimator + switch (from, to) { + case (nil, let to?): + animator = behavior.addAnimator(self.viewcontroller, to: to) + case (let from?, let to?): + animator = behavior.moveAnimator(self.viewcontroller, from: from, to: to) + case (let from?, nil): + animator = behavior.removeAnimator(self.viewcontroller, from: from) + case (nil, nil): + fatalError() + } + animator.addAnimations { [weak self] in guard let self = self else { return } - self.updateLayout(to: nil) + self.updateLayout(to: to) + if let to = to { + self.state = to + } } animator.addCompletion { _ in completion?() } animator.startAnimation() } else { - self.updateLayout(to: nil) + self.updateLayout(to: to) + if let to = to { + self.state = to + } completion?() } } diff --git a/Framework/Sources/FloatingPanelBehavior.swift b/Framework/Sources/FloatingPanelBehavior.swift index 1d279fa9..ce6102da 100644 --- a/Framework/Sources/FloatingPanelBehavior.swift +++ b/Framework/Sources/FloatingPanelBehavior.swift @@ -6,21 +6,38 @@ import UIKit public protocol FloatingPanelBehavior { - // Returns a UIViewPropertyAnimator object for interacting with a floating panel by a user pan gesture + /// Returns a UIViewPropertyAnimator object for interacting with a floating panel by a user pan gesture func interactionAnimator(_ fpc: FloatingPanelController, to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator - // Returns a UIViewPropertyAnimator object to present a floating panel - func presentAnimator(_ fpc: FloatingPanelController, from: FloatingPanelPosition, to: FloatingPanelPosition) -> UIViewPropertyAnimator - // Returns a UIViewPropertyAnimator object to dismiss a floating panel - func dismissAnimator(_ fpc: FloatingPanelController, from: FloatingPanelPosition) -> UIViewPropertyAnimator + /// Returns a UIViewPropertyAnimator object to add a floating panel to a position. + /// + /// Its animator instance will be used to animate the surface view in `FloatingPanelController.addPanel(toParent:belowView:animated:)`. + /// Default is an animator with ease-in-out curve and 0.25 sec duration. + func addAnimator(_ fpc: FloatingPanelController, to: FloatingPanelPosition) -> UIViewPropertyAnimator + + /// Returns a UIViewPropertyAnimator object to remove a floating panel from a position. + /// + /// Its animator instance will be used to animate the surface view in `FloatingPanelController.removePanelFromParent(animated:completion:)`. + /// Default is an animator with ease-in-out curve and 0.25 sec duration. + func removeAnimator(_ fpc: FloatingPanelController, from: FloatingPanelPosition) -> UIViewPropertyAnimator + + /// Returns a UIViewPropertyAnimator object to move a floating panel from a position to a position. + /// + /// Its animator instance will be used to animate the surface view in `FloatingPanelController.move(to:animated:completion:)`. + /// Default is an animator with ease-in-out curve and 0.25 sec duration. + func moveAnimator(_ fpc: FloatingPanelController, from: FloatingPanelPosition, to: FloatingPanelPosition) -> UIViewPropertyAnimator } public extension FloatingPanelBehavior { - func presentAnimator(_ fpc: FloatingPanelController, from: FloatingPanelPosition, to: FloatingPanelPosition) -> UIViewPropertyAnimator { + func addAnimator(_ fpc: FloatingPanelController, to: FloatingPanelPosition) -> UIViewPropertyAnimator { + return UIViewPropertyAnimator(duration: 0.25, curve: .easeInOut) + } + + func removeAnimator(_ fpc: FloatingPanelController, from: FloatingPanelPosition) -> UIViewPropertyAnimator { return UIViewPropertyAnimator(duration: 0.25, curve: .easeInOut) } - func dismissAnimator(_ fpc: FloatingPanelController, from: FloatingPanelPosition) -> UIViewPropertyAnimator { + func moveAnimator(_ fpc: FloatingPanelController, from: FloatingPanelPosition, to: FloatingPanelPosition) -> UIViewPropertyAnimator { return UIViewPropertyAnimator(duration: 0.25, curve: .easeInOut) } } From 8ce9f790a7020c1cc7a32d3fe375fafabd90232c Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Sat, 27 Oct 2018 17:36:13 +0900 Subject: [PATCH 026/623] Add a sample code to test FloatingPanelController.move(to:animated:) --- .../Sources/Base.lproj/Main.storyboard | 28 +++++++++++++++++++ Examples/Samples/Sources/ViewController.swift | 10 +++++++ 2 files changed, 38 insertions(+) diff --git a/Examples/Samples/Sources/Base.lproj/Main.storyboard b/Examples/Samples/Sources/Base.lproj/Main.storyboard index d63a963c..808e3f15 100644 --- a/Examples/Samples/Sources/Base.lproj/Main.storyboard +++ b/Examples/Samples/Sources/Base.lproj/Main.storyboard @@ -189,14 +189,42 @@ + + + + + + + + + + diff --git a/Examples/Samples/Sources/ViewController.swift b/Examples/Samples/Sources/ViewController.swift index 0f17c698..2bb9aab9 100644 --- a/Examples/Samples/Sources/ViewController.swift +++ b/Examples/Samples/Sources/ViewController.swift @@ -303,6 +303,16 @@ class ModalViewController: UIViewController { @IBAction func close(sender: UIButton) { dismiss(animated: true, completion: nil) } + + @IBAction func moveToFull(sender: UIButton) { + fpc.move(to: .full, animated: true) + } + @IBAction func moveToHalf(sender: UIButton) { + fpc.move(to: .half, animated: true) + } + @IBAction func moveToTip(sender: UIButton) { + fpc.move(to: .tip, animated: true) + } } class TabBarViewController: UITabBarController {} From 75ed43dcd02419b4837d59a5cf76bc3999395aad Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Wed, 24 Oct 2018 09:22:29 +0900 Subject: [PATCH 027/623] Change the default landscape layout --- Framework/Sources/FloatingPanelLayout.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Framework/Sources/FloatingPanelLayout.swift b/Framework/Sources/FloatingPanelLayout.swift index 926573ba..8862bfcc 100644 --- a/Framework/Sources/FloatingPanelLayout.swift +++ b/Framework/Sources/FloatingPanelLayout.swift @@ -83,9 +83,9 @@ public class FloatingPanelDefaultLandscapeLayout: FloatingPanelLayout { public func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] { return [ - surfaceView.leftAnchor.constraint(equalTo: view.sideLayoutGuide.leftAnchor, constant: 8.0), - surfaceView.widthAnchor.constraint(equalToConstant: 291), - ] + surfaceView.leftAnchor.constraint(equalTo: view.sideLayoutGuide.leftAnchor, constant: 0.0), + surfaceView.rightAnchor.constraint(equalTo: view.sideLayoutGuide.rightAnchor, constant: 0.0), + ] } } From 661cc768faf33e9bcda20a354b3ece8b130b0a1d Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Wed, 24 Oct 2018 09:22:54 +0900 Subject: [PATCH 028/623] Modify a custom landscape layout for Maps --- Examples/Maps/Maps/ViewController.swift | 37 +++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/Examples/Maps/Maps/ViewController.swift b/Examples/Maps/Maps/ViewController.swift index 4c6bac56..3f15135e 100644 --- a/Examples/Maps/Maps/ViewController.swift +++ b/Examples/Maps/Maps/ViewController.swift @@ -84,15 +84,16 @@ class ViewController: UIViewController, MKMapViewDelegate, UISearchBarDelegate, // MARK: FloatingPanelControllerDelegate func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? { - switch traitCollection.verticalSizeClass { + switch newCollection.verticalSizeClass { case .compact: fpc.surfaceView.borderWidth = 1.0 / traitCollection.displayScale fpc.surfaceView.borderColor = UIColor.black.withAlphaComponent(0.2) + return SearchPanelLandscapeLayout() default: fpc.surfaceView.borderWidth = 0.0 fpc.surfaceView.borderColor = nil + return nil } - return nil } func floatingPanelDidMove(_ vc: FloatingPanelController) { @@ -203,6 +204,38 @@ class SearchPanelViewController: UIViewController, UITableViewDataSource, UITabl } } +public class SearchPanelLandscapeLayout: FloatingPanelLayout { + public var initialPosition: FloatingPanelPosition { + return .tip + } + + public var supportedPositions: Set { + return [.full, .tip] + } + + public func insetFor(position: FloatingPanelPosition) -> CGFloat? { + switch position { + case .full: return 16.0 + case .tip: return 69.0 + default: return nil + } + } + + public func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] { + if #available(iOS 11.0, *) { + return [ + surfaceView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 8.0), + surfaceView.widthAnchor.constraint(equalToConstant: 291), + ] + } else { + return [ + surfaceView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 8.0), + surfaceView.widthAnchor.constraint(equalToConstant: 291), + ] + } + } +} + class SearchCell: UITableViewCell { @IBOutlet weak var iconImageView: UIImageView! @IBOutlet weak var titleLabel: UILabel! From 2caf7a5cd2ad8e9f0feea4995258c8e71004865b Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Sun, 28 Oct 2018 07:16:58 +0900 Subject: [PATCH 029/623] Fix failure requirements of the pan gesture --- Framework/Sources/FloatingPanel.swift | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Framework/Sources/FloatingPanel.swift b/Framework/Sources/FloatingPanel.swift index 767b3619..b914189b 100644 --- a/Framework/Sources/FloatingPanel.swift +++ b/Framework/Sources/FloatingPanel.swift @@ -184,10 +184,17 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate guard gestureRecognizer == panGesture else { return false } // Do not begin the pan gesture until any other gestures fail except fo the tracking scrollView's pan gesture. - if otherGestureRecognizer == scrollView?.panGestureRecognizer { + switch otherGestureRecognizer { + case scrollView?.panGestureRecognizer: return false - } else { + case is UIPanGestureRecognizer, + is UISwipeGestureRecognizer, + is UIRotationGestureRecognizer, + is UIScreenEdgePanGestureRecognizer, + is UIPinchGestureRecognizer: return true + default: + return false } } From a3e1590481a3ea5200164e44a9c872b90a72c571 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Sun, 28 Oct 2018 07:30:43 +0900 Subject: [PATCH 030/623] Release v1.1.0 --- FloatingPanel.podspec | 2 +- Framework/Sources/Info.plist | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/FloatingPanel.podspec b/FloatingPanel.podspec index 0087f8bf..dbfa8ff2 100644 --- a/FloatingPanel.podspec +++ b/FloatingPanel.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "FloatingPanel" - s.version = "1.0.0" + s.version = "1.1.0" s.summary = "FloatingPanel is a simple and easy-to-use UI component of a floating panel interface" s.description = <<-DESC FloatingPanel is a simple and easy-to-use UI component for a new interface introduced in Apple Maps, Shortcuts and Stocks app. diff --git a/Framework/Sources/Info.plist b/Framework/Sources/Info.plist index e1fe4cfb..a4cf4c45 100644 --- a/Framework/Sources/Info.plist +++ b/Framework/Sources/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 1.0 + 1.1.0 CFBundleVersion $(CURRENT_PROJECT_VERSION) From 535f93fba994d4237cfda3bcb049d97eafe4c96e Mon Sep 17 00:00:00 2001 From: kingcos <2821836721v@gmail.com> Date: Mon, 29 Oct 2018 11:16:59 +0800 Subject: [PATCH 031/623] Add missing constraint of the title --- .../Stocks/Stocks/Base.lproj/Main.storyboard | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/Examples/Stocks/Stocks/Base.lproj/Main.storyboard b/Examples/Stocks/Stocks/Base.lproj/Main.storyboard index a9e759be..343555a9 100644 --- a/Examples/Stocks/Stocks/Base.lproj/Main.storyboard +++ b/Examples/Stocks/Stocks/Base.lproj/Main.storyboard @@ -1,6 +1,6 @@ - + @@ -15,11 +15,11 @@ - + - + @@ -34,10 +34,10 @@ - + - + @@ -52,18 +52,17 @@ - - - + + - + @@ -390,14 +390,36 @@ + + + + + + + + + @@ -413,6 +435,7 @@ + @@ -432,7 +455,7 @@ - + @@ -507,4 +530,7 @@ Section 1.10.33 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC + + + diff --git a/Examples/Samples/Sources/ViewController.swift b/Examples/Samples/Sources/ViewController.swift index 83dc7207..baee1da0 100644 --- a/Examples/Samples/Sources/ViewController.swift +++ b/Examples/Samples/Sources/ViewController.swift @@ -70,9 +70,10 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable mainPanelVC.surfaceView.cornerRadius = 6.0 mainPanelVC.surfaceView.shadowHidden = false - // Add a content view controller and connect with the scroll view - mainPanelVC.show(contentVC, sender: self) + // Set a content view controller + mainPanelVC.set(contentViewController: contentVC) + // Track a scroll view switch contentVC { case let consoleVC as DebugTextViewController: mainPanelVC.track(scrollView: consoleVC.textView) @@ -128,10 +129,8 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable detailPanelVC.surfaceView.cornerRadius = 6.0 detailPanelVC.surfaceView.shadowHidden = false - // Add a content view controller and connect with the scroll view - detailPanelVC.show(contentVC, sender: self) - - // (contentVC as? DetailViewController)?.closeButton?.addTarget(self, action: #selector(dismissDetailPanelVC), for: .touchUpInside) + // Set a content view controller + detailPanelVC.set(contentViewController: contentVC) // Add FloatingPanel to self.view detailPanelVC.addPanel(toParent: self, belowView: nil, animated: true) @@ -299,6 +298,18 @@ class DetailViewController: UIViewController { // dismiss(animated: true, completion: nil) (self.parent as? FloatingPanelController)?.removePanelFromParent(animated: true, completion: nil) } + + @IBAction func buttonPressed(_ sender: UIButton) { + switch sender.titleLabel?.text { + case "Show": + performSegue(withIdentifier: "ShowSegue", sender: self) + case "Present Modally": + performSegue(withIdentifier: "PresentModallySegue", sender: self) + default: + break + } + } + @IBAction func tapped(_ sender: Any) { print("Detail panel is tapped!") } @@ -323,12 +334,13 @@ class ModalViewController: UIViewController { fpc.surfaceView.cornerRadius = 6.0 fpc.surfaceView.shadowHidden = false - // Add a content view controller and connect with the scroll view + // Set a content view controller and track the scroll view let consoleVC = storyboard?.instantiateViewController(withIdentifier: "ConsoleViewController") as! DebugTextViewController - fpc.show(consoleVC, sender: self) - self.consoleVC = consoleVC + fpc.set(contentViewController: consoleVC) fpc.track(scrollView: consoleVC.textView) + self.consoleVC = consoleVC + // Add FloatingPanel to self.view fpc.addPanel(toParent: self, belowView: safeAreaView) } @@ -370,11 +382,11 @@ class TabBarContentViewController: UIViewController, FloatingPanelControllerDele fpc.surfaceView.cornerRadius = 6.0 fpc.surfaceView.shadowHidden = false - // Add a content view controller and connect with the scroll view + // Set a content view controller and track the scroll view let consoleVC = storyboard?.instantiateViewController(withIdentifier: "ConsoleViewController") as! DebugTextViewController - fpc.show(consoleVC, sender: self) - self.consoleVC = consoleVC + fpc.set(contentViewController: consoleVC) fpc.track(scrollView: consoleVC.textView) + self.consoleVC = consoleVC // Add FloatingPanel to self.view fpc.addPanel(toParent: self) diff --git a/Examples/Stocks/Stocks/ViewController.swift b/Examples/Stocks/Stocks/ViewController.swift index 9081d68e..bb4ab270 100644 --- a/Examples/Stocks/Stocks/ViewController.swift +++ b/Examples/Stocks/Stocks/ViewController.swift @@ -34,8 +34,8 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate { newsVC = storyboard?.instantiateViewController(withIdentifier: "News") as? NewsViewController - // Add a content view controller - fpc.show(newsVC, sender: self) + // Set a content view controller + fpc.set(contentViewController: newsVC) fpc.track(scrollView: newsVC.scrollView) fpc.addPanel(toParent: self, belowView: bottomToolView, animated: false) diff --git a/Framework/Sources/FloatingPanelController.swift b/Framework/Sources/FloatingPanelController.swift index 5f2582c7..52671b76 100644 --- a/Framework/Sources/FloatingPanelController.swift +++ b/Framework/Sources/FloatingPanelController.swift @@ -97,6 +97,13 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI get { return floatingPanel.isRemovalInteractionEnabled } } + /// The view controller responsible for the content portion of the floating panel. + public var contentViewController: UIViewController? { + set { set(contentViewController: newValue) } + get { return _contentViewController } + } + private var _contentViewController: UIViewController? + private var floatingPanel: FloatingPanel! required init?(coder aDecoder: NSCoder) { @@ -252,20 +259,44 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI floatingPanel.move(to: to, animated: animated, completion: completion) } - /// Presents the specified view controller as the content view controller in the surface view interface. + /// Sets the view controller responsible for the content portion of the floating panel.. + public func set(contentViewController: UIViewController?) { + if let vc = _contentViewController { + vc.willMove(toParent: nil) + vc.view.removeFromSuperview() + vc.removeFromParent() + } + + if let vc = contentViewController { + let surfaceView = self.view as! FloatingPanelSurfaceView + surfaceView.contentView.addSubview(vc.view) + vc.view.frame = surfaceView.contentView.bounds + vc.view.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + vc.view.topAnchor.constraint(equalTo: surfaceView.contentView.topAnchor, constant: 0.0), + vc.view.leftAnchor.constraint(equalTo: surfaceView.contentView.leftAnchor, constant: 0.0), + vc.view.rightAnchor.constraint(equalTo: surfaceView.contentView.rightAnchor, constant: 0.0), + vc.view.bottomAnchor.constraint(equalTo: surfaceView.contentView.bottomAnchor, constant: 0.0), + ]) + addChild(vc) + vc.didMove(toParent: self) + } + + _contentViewController = contentViewController + } + + @available(*, unavailable, renamed: "set(contentViewController:)") public override func show(_ vc: UIViewController, sender: Any?) { - let surfaceView = self.view as! FloatingPanelSurfaceView - surfaceView.contentView.addSubview(vc.view) - vc.view.frame = surfaceView.contentView.bounds - vc.view.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - vc.view.topAnchor.constraint(equalTo: surfaceView.contentView.topAnchor, constant: 0.0), - vc.view.leftAnchor.constraint(equalTo: surfaceView.contentView.leftAnchor, constant: 0.0), - vc.view.rightAnchor.constraint(equalTo: surfaceView.contentView.rightAnchor, constant: 0.0), - vc.view.bottomAnchor.constraint(equalTo: surfaceView.contentView.bottomAnchor, constant: 0.0), - ]) - addChild(vc) - vc.didMove(toParent: self) + if let target = self.parent?.targetViewController(forAction: #selector(UIViewController.show(_:sender:)), sender: sender) { + target.show(vc, sender: sender) + } + } + + @available(*, unavailable, renamed: "set(contentViewController:)") + public override func showDetailViewController(_ vc: UIViewController, sender: Any?) { + if let target = self.parent?.targetViewController(forAction: #selector(UIViewController.showDetailViewController(_:sender:)), sender: sender) { + target.showDetailViewController(vc, sender: sender) + } } // MARK: - Scroll view tracking diff --git a/README.md b/README.md index 5aba6462..39521445 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ The new interface displays the related contents and utilities in parallel as a u - [Getting Started](#getting-started) - [Usage](#usage) - [Customize the layout of a floating panel with `FloatingPanelLayout` protocol](#customize-the-layout-of-a-floating-panel-with--floatingpanellayout-protocol) - - [Change the initial position, supported positions and height](#change-the-initial-position-supported-positions-and-height) + - [Change the initial position and height](#change-the-initial-position-and-height) - [Support your landscape layout](#support-your-landscape-layout) - [Customize the behavior with `FloatingPanelBehavior` protocol](#customize-the-behavior-with-floatingpanelbehavior-protocol) - [Modify your floating panel's interaction](#modify-your-floating-panels-interaction) @@ -33,6 +33,7 @@ The new interface displays the related contents and utilities in parallel as a u - [Move a position with an animation](#move-a-position-with-an-animation) - [Make your contents correspond with a floating panel behavior](#make-your-contents-correspond-with-a-floating-panel-behavior) - [Notes](#notes) + - ['Show' or 'Show Detail' Segues from `FloatingPanelController`'s content view controller](#show-or-show-detail-segues-from-floatingpanelcontrollers-content-view-controller) - [FloatingPanelSurfaceView's issue on iOS 10](#floatingpanelsurfaceviews-issue-on-ios-10) - [Author](#author) - [License](#license) @@ -96,14 +97,14 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate { // Assign self as the delegate of the controller. fpc.delegate = self // Optional - // Add a content view controller. + // Set a content view controller. let contentVC = ContentViewController() - fpc.show(contentVC, sender: nil) + fpc.set(viewController: contentVC) // Track a scroll view(or the siblings) in the content view controller. fpc.track(scrollView: contentVC.tableView) - // Add the views managed by the `FloatingPanelController` object to self.view. + // Add and show the views managed by the `FloatingPanelController` object to self.view. fpc.addPanel(toParent: self) } @@ -222,7 +223,7 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate { self.searchPanelVC = FloatingPanelController() let searchVC = SearchViewController() - self.searchPanelVC.show(searchVC, sender: nil) + self.searchPanelVC.set(viewController: searchVC) self.searchPanelVC.track(scrollView: contentVC.tableView) self.searchPanelVC.addPanel(toParent: self) @@ -231,7 +232,7 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate { self.detailPanelVC = FloatingPanelController() let contentVC = ContentViewController() - self.detailPanelVC.show(contentVC, sender: nil) + self.searchPanelVC.set(viewController: contentVC) self.detailPanelVC.track(scrollView: contentVC.scrollView) self.detailPanelVC.addPanel(toParent: self) @@ -279,6 +280,39 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate { ## Notes +### 'Show' or 'Show Detail' Segues from `FloatingPanelController`'s content view controller + +'Show' or 'Show Detail' segues from a content view controller will be managed by a view controller(hereinafter called 'master VC') adding a floating panel. Because a floating panel is just a subview of the master VC. + +`FloatingPanelController` has no way to manage a stack of view controllers like `UINavigationController`. If so, it would be so complicated and the interface will become `UINavigationController`. This component should not have the responsibility to manage the stack. + +By the way, a content view controller can present a view controller modally with `present(_:animated:completion:)` or 'Present Modally' segue. + +However, sometimes you want to show a destination view controller of 'Show' or 'Show Detail' segue with another floating panel. It's possible to override `show(_:sender)` of the master VC! + +Here is an example. + +```swift +class ViewController: UIViewController { + var fpc: FloatingPanelController! + var secondFpc: FloatingPanelController! + + ... + override func show(_ vc: UIViewController, sender: Any?) { + secondFpc = FloatingPanelController() + + secondFpc.set(contentViewController: vc) + + secondFpc.addPanel(toParent: self) + } + ... +} +``` + +A `FloatingPanelController` object proxies an action for `show(_:sender)` to the master VC. That's why the master VC can handle a destination view controller of a 'Show' or 'Show Detail' segue and you can hook `show(_:sender)` to show a secondally floating panel set the destination view controller to the content. + +It's a greate way to decouple between a floating panel and the content VC. + ### FloatingPanelSurfaceView's issue on iOS 10 * On iOS 10, `FloatingPanelSurfaceView.cornerRadius` isn't not automatically masked with the top rounded corners because of UIVisualEffectView issue. See https://forums.developer.apple.com/thread/50854. From fd7b771a39cd2d060d825ccc554333bbc14d1513 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Fri, 2 Nov 2018 12:14:50 +0900 Subject: [PATCH 040/623] Fix README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 39521445..a7a42b94 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate { // Set a content view controller. let contentVC = ContentViewController() - fpc.set(viewController: contentVC) + fpc.set(contentViewController: contentVC) // Track a scroll view(or the siblings) in the content view controller. fpc.track(scrollView: contentVC.tableView) @@ -223,7 +223,7 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate { self.searchPanelVC = FloatingPanelController() let searchVC = SearchViewController() - self.searchPanelVC.set(viewController: searchVC) + self.searchPanelVC.set(contentViewController: searchVC) self.searchPanelVC.track(scrollView: contentVC.tableView) self.searchPanelVC.addPanel(toParent: self) @@ -232,7 +232,7 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate { self.detailPanelVC = FloatingPanelController() let contentVC = ContentViewController() - self.searchPanelVC.set(viewController: contentVC) + self.detailPanelVC.set(contentViewController: contentVC) self.detailPanelVC.track(scrollView: contentVC.scrollView) self.detailPanelVC.addPanel(toParent: self) From e75fd57edfa76ee7babc7afcdcb59b61f6205ffc Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Fri, 2 Nov 2018 12:18:26 +0900 Subject: [PATCH 041/623] Revert a sample code in README to prevent a confusion for v1.1.0 --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a7a42b94..a6724c13 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate { // Set a content view controller. let contentVC = ContentViewController() - fpc.set(contentViewController: contentVC) + fpc.show(contentVC, sender: nil) // Track a scroll view(or the siblings) in the content view controller. fpc.track(scrollView: contentVC.tableView) @@ -223,7 +223,7 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate { self.searchPanelVC = FloatingPanelController() let searchVC = SearchViewController() - self.searchPanelVC.set(contentViewController: searchVC) + self.searchPanelVC.show(searchVC, sender: nil) self.searchPanelVC.track(scrollView: contentVC.tableView) self.searchPanelVC.addPanel(toParent: self) @@ -232,7 +232,7 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate { self.detailPanelVC = FloatingPanelController() let contentVC = ContentViewController() - self.detailPanelVC.set(contentViewController: contentVC) + self.detailPanelVC.show(contentVC, sender: nil) self.detailPanelVC.track(scrollView: contentVC.scrollView) self.detailPanelVC.addPanel(toParent: self) From 192e72d2950e2aa3e0d9ec48b56ac9c007d9f223 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Mon, 29 Oct 2018 09:01:03 +0900 Subject: [PATCH 042/623] Clean up FloatingPanelSurfaceView and FloatingPanelLayout --- .../Sources/FloatingPanelController.swift | 10 +-------- Framework/Sources/FloatingPanelLayout.swift | 15 ++++++------- .../Sources/FloatingPanelSurfaceView.swift | 21 +++++++++++++++---- 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/Framework/Sources/FloatingPanelController.swift b/Framework/Sources/FloatingPanelController.swift index 52671b76..6ad2e660 100644 --- a/Framework/Sources/FloatingPanelController.swift +++ b/Framework/Sources/FloatingPanelController.swift @@ -269,15 +269,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI if let vc = contentViewController { let surfaceView = self.view as! FloatingPanelSurfaceView - surfaceView.contentView.addSubview(vc.view) - vc.view.frame = surfaceView.contentView.bounds - vc.view.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - vc.view.topAnchor.constraint(equalTo: surfaceView.contentView.topAnchor, constant: 0.0), - vc.view.leftAnchor.constraint(equalTo: surfaceView.contentView.leftAnchor, constant: 0.0), - vc.view.rightAnchor.constraint(equalTo: surfaceView.contentView.rightAnchor, constant: 0.0), - vc.view.bottomAnchor.constraint(equalTo: surfaceView.contentView.bottomAnchor, constant: 0.0), - ]) + surfaceView.add(childView: vc.view) addChild(vc) vc.didMove(toParent: self) } diff --git a/Framework/Sources/FloatingPanelLayout.swift b/Framework/Sources/FloatingPanelLayout.swift index 5bb074fe..a252a3cd 100644 --- a/Framework/Sources/FloatingPanelLayout.swift +++ b/Framework/Sources/FloatingPanelLayout.swift @@ -115,7 +115,7 @@ class FloatingPanelLayoutAdapter { private var halfConstraints: [NSLayoutConstraint] = [] private var tipConstraints: [NSLayoutConstraint] = [] private var offConstraints: [NSLayoutConstraint] = [] - private var heightConstraints: NSLayoutConstraint? = nil + private var heightConstraints: [NSLayoutConstraint] = [] private var fullInset: CGFloat { return layout.insetFor(position: .full) ?? 0.0 @@ -218,15 +218,12 @@ class FloatingPanelLayoutAdapter { } } - if let consts = self.heightConstraints { - NSLayoutConstraint.deactivate([consts]) - } - + NSLayoutConstraint.deactivate(heightConstraints) let height = UIScreen.main.bounds.height - (safeAreaInsets.top + fullInset) - let consts = surfaceView.heightAnchor.constraint(equalToConstant: height) - - NSLayoutConstraint.activate([consts]) - heightConstraints = consts + heightConstraints = [ + surfaceView.heightAnchor.constraint(equalToConstant: height) + ] + NSLayoutConstraint.activate(heightConstraints) surfaceView.set(bottomOverflow: heightBuffer) } diff --git a/Framework/Sources/FloatingPanelSurfaceView.swift b/Framework/Sources/FloatingPanelSurfaceView.swift index 8ffd83dc..f653e5b6 100644 --- a/Framework/Sources/FloatingPanelSurfaceView.swift +++ b/Framework/Sources/FloatingPanelSurfaceView.swift @@ -76,6 +76,10 @@ public class FloatingPanelSurfaceView: UIView { super.backgroundColor = .clear self.clipsToBounds = false + let shadowLayer = CAShapeLayer() + layer.insertSublayer(shadowLayer, at: 0) + self.shadowLayer = shadowLayer + let contentView = FloatingPanelSurfaceContentView() addSubview(contentView) self.contentView = contentView as UIView @@ -99,10 +103,6 @@ public class FloatingPanelSurfaceView: UIView { grabberHandle.heightAnchor.constraint(equalToConstant: grabberHandle.frame.height), grabberHandle.centerXAnchor.constraint(equalTo: centerXAnchor), ]) - - let shadowLayer = CAShapeLayer() - layer.insertSublayer(shadowLayer, at: 0) - self.shadowLayer = shadowLayer } public override func layoutSubviews() { @@ -159,4 +159,17 @@ public class FloatingPanelSurfaceView: UIView { updateShadowLayer() updateContentViewMask() } + + + func add(childView: UIView) { + contentView.addSubview(childView) + childView.frame = contentView.bounds + childView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + childView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 0.0), + childView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 0.0), + childView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: 0.0), + childView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: 0.0), + ]) + } } From 50043bbea331da1a0abc2838005b43ff36430bc1 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Fri, 2 Nov 2018 15:32:34 +0900 Subject: [PATCH 043/623] Observe parent's view.safeAreaInsets --- .../Sources/FloatingPanelController.swift | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Framework/Sources/FloatingPanelController.swift b/Framework/Sources/FloatingPanelController.swift index 6ad2e660..82e7c0f8 100644 --- a/Framework/Sources/FloatingPanelController.swift +++ b/Framework/Sources/FloatingPanelController.swift @@ -105,6 +105,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI private var _contentViewController: UIViewController? private var floatingPanel: FloatingPanel! + private var layoutInsetsObservations: [NSKeyValueObservation] = [] required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) @@ -216,6 +217,21 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI parent.view.addSubview(self.view) } + layoutInsetsObservations.removeAll() + + // Must track safeAreaInsets/{top,bottom}LayoutGuide of the `parent.view` to update floatingPanel.safeAreaInsets`. + // Because the parent VC does not call viewSafeAreaInsetsDidChange() expectedly on the bottom inset's update. + // So I needs to observe them. It ensures that the `adjustedContentInsets` has a correct value. + if #available(iOS 11.0, *) { + let observaion = parent.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() + } + parent.addChild(self) // Must set a layout again here because `self.traitCollection` is applied correctly once it's added to a parent VC @@ -240,6 +256,8 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI return } + layoutInsetsObservations.removeAll() + floatingPanel.dismiss(animated: animated) { [weak self] in guard let self = self else { return } From 13182124b4b150f6cb0b45fcb75ab8134e1b2820 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Fri, 2 Nov 2018 19:51:06 +0900 Subject: [PATCH 044/623] Fix the surface view height on non traslucent nav bar --- Framework/Sources/FloatingPanelLayout.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Framework/Sources/FloatingPanelLayout.swift b/Framework/Sources/FloatingPanelLayout.swift index a252a3cd..38f05d62 100644 --- a/Framework/Sources/FloatingPanelLayout.swift +++ b/Framework/Sources/FloatingPanelLayout.swift @@ -109,6 +109,7 @@ class FloatingPanelLayoutAdapter { } } + private var parentHeight: CGFloat = 0.0 private var heightBuffer: CGFloat = 88.0 // For bounce private var fixedConstraints: [NSLayoutConstraint] = [] private var fullConstraints: [NSLayoutConstraint] = [] @@ -172,6 +173,8 @@ class FloatingPanelLayoutAdapter { } func prepareLayout(toParent parent: UIViewController) { + parentHeight = parent.view.frame.height + surfaceView.translatesAutoresizingMaskIntoConstraints = false backdropVIew.translatesAutoresizingMaskIntoConstraints = false @@ -219,7 +222,7 @@ class FloatingPanelLayoutAdapter { } NSLayoutConstraint.deactivate(heightConstraints) - let height = UIScreen.main.bounds.height - (safeAreaInsets.top + fullInset) + let height = parentHeight - (safeAreaInsets.top + fullInset) heightConstraints = [ surfaceView.heightAnchor.constraint(equalToConstant: height) ] From 7943f2a67f711c9b6bcc552f53b4e9468a1a7540 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Sun, 4 Nov 2018 11:21:08 +0900 Subject: [PATCH 045/623] Fix a bug in moving interaction from full position The floating panel must work well if the tracking scroll view's content offset isn't the top. --- Framework/Sources/FloatingPanel.swift | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/Framework/Sources/FloatingPanel.swift b/Framework/Sources/FloatingPanel.swift index 52e21aed..f314899d 100644 --- a/Framework/Sources/FloatingPanel.swift +++ b/Framework/Sources/FloatingPanel.swift @@ -44,6 +44,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate private var animator: UIViewPropertyAnimator? private var initialFrame: CGRect = .zero + private var initialScrollOffset: CGPoint = .zero private var transOffsetY: CGFloat = 0 private var interactionInProgress: Bool = false @@ -222,7 +223,20 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate case scrollView?.panGestureRecognizer: guard let scrollView = scrollView else { return } if surfaceView.frame.minY > layoutAdapter.topY { - scrollView.contentOffset.y = scrollView.contentOffsetZero.y + switch state { + case .full: + // Prevent over scrolling from scroll top in moving the panel from full. + scrollView.contentOffset.y = scrollView.contentOffsetZero.y + case .half, .tip: + guard scrollView.isDecelerating == false else { + // Don't fix the scroll offset in animating the panel to half and tip. + // It causes a buggy scrolling deceleration because `state` becomes + // a target position in animating the panel on the interaction from full. + return + } + // Fix the scroll offset in moving the panel from half and tip. + scrollView.contentOffset = initialScrollOffset + } } case panGesture: let translation = panGesture.translation(in: panGesture.view!.superview) @@ -231,7 +245,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate log.debug(panGesture.state, ">>>", "{ translation: \(translation), velocity: \(velocity) }") - if let scrollView = scrollView, scrollView.frame.contains(location) { + if let scrollView = scrollView, scrollView.frame.contains(location), interactionInProgress == false { log.debug("ScrollView.contentOffset >>>", scrollView.contentOffset) if state == .full { if scrollView.contentOffset.y - scrollView.contentOffsetZero.y > 0 { @@ -240,7 +254,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate if scrollView.isDecelerating { return } - if interactionInProgress == false, velocity.y < 0 || velocity.y > 2500.0 { + if velocity.y < 0 || velocity.y > 2500.0 { return } } @@ -267,8 +281,8 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate private func panningBegan() { // A user interaction does not always start from Began state of the pan gesture // because it can be recognized in scrolling a content in a content view controller. - // So I do nothing here. - log.debug("panningBegan \(initialFrame)") + // So do nothing here. + log.debug("panningBegan") } private func panningChange(with translation: CGPoint) { @@ -321,6 +335,9 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate private func startInteraction(with translation: CGPoint) { log.debug("startInteraction") initialFrame = surfaceView.frame + if let scrollView = scrollView { + initialScrollOffset = scrollView.contentOffset + } transOffsetY = translation.y viewcontroller.delegate?.floatingPanelWillBeginDragging(viewcontroller) From 2da2563ef55bb87c1659cec3539af6fb68fa4704 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Sun, 4 Nov 2018 10:58:29 +0900 Subject: [PATCH 046/623] Fix the removal interaction --- Framework/Sources/FloatingPanel.swift | 26 ++++++++++++++----- .../Sources/FloatingPanelController.swift | 8 ++++++ Framework/Sources/FloatingPanelLayout.swift | 6 ++++- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/Framework/Sources/FloatingPanel.swift b/Framework/Sources/FloatingPanel.swift index 52e21aed..769fcca0 100644 --- a/Framework/Sources/FloatingPanel.swift +++ b/Framework/Sources/FloatingPanel.swift @@ -297,22 +297,36 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate endInteraction(for: targetPosition) - viewcontroller.delegate?.floatingPanelDidEndDragging(viewcontroller, withVelocity: velocity, targetPosition: targetPosition) - if isRemovalInteractionEnabled, isBottomState { + let posY = layoutAdapter.positionY(for: state) + let currentY = getCurrentY(from: initialFrame, with: translation) + let safeAreaBottomY = layoutAdapter.safeAreaBottomY let vth = behavior.removalVelocityThreshold let velocityVector = (distance != 0) ? CGVector(dx: 0, dy: max(min(velocity.y/distance, vth), 0.0)) : .zero - if velocityVector.dy == vth { - let animator = behavior.removalInteractionAnimator(self.viewcontroller, with: velocityVector) + + let startRemovalAnimation = { + let animator = self.behavior.removalInteractionAnimator(self.viewcontroller, with: velocityVector) animator.addAnimations { [weak self] in guard let self = self else { return } self.updateLayout(to: nil) - } + } + animator.addCompletion({ [weak self] (_) in + guard let self = self else { return } + self.viewcontroller.delegate?.floatingPanelDidEndRemove(self.viewcontroller) + }) animator.startAnimation() - return + } + + if (currentY - posY) != 0 { + if (safeAreaBottomY - posY) / (currentY - posY) >= 0.5 || velocityVector.dy == vth { + viewcontroller.delegate?.floatingPanelDidEndDraggingToRemove(viewcontroller, withVelocity: velocity) + startRemovalAnimation() + return + } } } + viewcontroller.delegate?.floatingPanelDidEndDragging(viewcontroller, withVelocity: velocity, targetPosition: targetPosition) viewcontroller.delegate?.floatingPanelWillBeginDecelerating(viewcontroller) startAnimation(to: targetPosition, at: distance, with: velocity) diff --git a/Framework/Sources/FloatingPanelController.swift b/Framework/Sources/FloatingPanelController.swift index 52671b76..a207ed76 100644 --- a/Framework/Sources/FloatingPanelController.swift +++ b/Framework/Sources/FloatingPanelController.swift @@ -20,6 +20,11 @@ public protocol FloatingPanelControllerDelegate: class { func floatingPanelDidEndDragging(_ vc: FloatingPanelController, withVelocity velocity: CGPoint, targetPosition: FloatingPanelPosition) func floatingPanelWillBeginDecelerating(_ vc: FloatingPanelController) // called on finger up as we are moving func floatingPanelDidEndDecelerating(_ vc: FloatingPanelController) // called when scroll view grinds to a halt + + // called on start of dragging to remove its views from a parent view controller + func floatingPanelDidEndDraggingToRemove(_ vc: FloatingPanelController, withVelocity velocity: CGPoint) + // called when its views are removed from a parent view controller + func floatingPanelDidEndRemove(_ vc: FloatingPanelController) } public extension FloatingPanelControllerDelegate { @@ -34,6 +39,9 @@ public extension FloatingPanelControllerDelegate { func floatingPanelDidEndDragging(_ vc: FloatingPanelController, withVelocity velocity: CGPoint, targetPosition: FloatingPanelPosition) {} func floatingPanelWillBeginDecelerating(_ vc: FloatingPanelController) {} func floatingPanelDidEndDecelerating(_ vc: FloatingPanelController) {} + + func floatingPanelDidEndDraggingToRemove(_ vc: FloatingPanelController, withVelocity velocity: CGPoint) {} + func floatingPanelDidEndRemove(_ vc: FloatingPanelController) {} } public enum FloatingPanelPosition: Int, CaseIterable { diff --git a/Framework/Sources/FloatingPanelLayout.swift b/Framework/Sources/FloatingPanelLayout.swift index 5bb074fe..415f1fc5 100644 --- a/Framework/Sources/FloatingPanelLayout.swift +++ b/Framework/Sources/FloatingPanelLayout.swift @@ -147,6 +147,10 @@ class FloatingPanelLayoutAdapter { } } + var safeAreaBottomY: CGFloat { + return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom) + } + var adjustedContentInsets: UIEdgeInsets { return UIEdgeInsets(top: 0.0, left: 0.0, @@ -205,7 +209,7 @@ class FloatingPanelLayoutAdapter { constant: -tipInset), ] offConstraints = [ - surfaceView.topAnchor.constraint(equalTo: parent.layoutGuide.bottomAnchor, constant: 0.0), + surfaceView.topAnchor.constraint(equalTo: parent.view.bottomAnchor, constant: 0.0), ] } From ff3d82ee78037ece778eba283fc0aa56271c06f5 Mon Sep 17 00:00:00 2001 From: Florian Fittschen Date: Mon, 5 Nov 2018 11:15:54 +0100 Subject: [PATCH 047/623] Fix moving without scrolling to top --- Framework/Sources/FloatingPanel.swift | 56 +++++++++++++++-------- Framework/Sources/GrabberHandleView.swift | 6 +++ 2 files changed, 44 insertions(+), 18 deletions(-) diff --git a/Framework/Sources/FloatingPanel.swift b/Framework/Sources/FloatingPanel.swift index f314899d..620eddde 100644 --- a/Framework/Sources/FloatingPanel.swift +++ b/Framework/Sources/FloatingPanel.swift @@ -222,11 +222,14 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate switch panGesture { case scrollView?.panGestureRecognizer: guard let scrollView = scrollView else { return } + if panGesture.state == .began { + initialScrollOffset = scrollView.contentOffset + } if surfaceView.frame.minY > layoutAdapter.topY { switch state { case .full: // Prevent over scrolling from scroll top in moving the panel from full. - scrollView.contentOffset.y = scrollView.contentOffsetZero.y + scrollView.contentOffset.y = initialScrollOffset.y case .half, .tip: guard scrollView.isDecelerating == false else { // Don't fix the scroll offset in animating the panel to half and tip. @@ -235,7 +238,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate return } // Fix the scroll offset in moving the panel from half and tip. - scrollView.contentOffset = initialScrollOffset + scrollView.contentOffset.y = initialScrollOffset.y } } case panGesture: @@ -245,19 +248,8 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate log.debug(panGesture.state, ">>>", "{ translation: \(translation), velocity: \(velocity) }") - if let scrollView = scrollView, scrollView.frame.contains(location), interactionInProgress == false { - log.debug("ScrollView.contentOffset >>>", scrollView.contentOffset) - if state == .full { - if scrollView.contentOffset.y - scrollView.contentOffsetZero.y > 0 { - return - } - if scrollView.isDecelerating { - return - } - if velocity.y < 0 || velocity.y > 2500.0 { - return - } - } + if shouldScrollViewHandleTouch(scrollView, point: location, velocity: velocity) { + return } switch panGesture.state { @@ -278,6 +270,37 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate } } + private func shouldScrollViewHandleTouch(_ scrollView: UIScrollView?, point: CGPoint, velocity: CGPoint) -> Bool { + let grabberBarFrame = CGRect(x: surfaceView.bounds.origin.x, + y: surfaceView.bounds.origin.y, + width: surfaceView.bounds.width, + height: FloatingPanelSurfaceView.topGrabberBarHeight * 2) + + guard + let scrollView = scrollView, // When no scrollView, nothing to handle. + state == .full, // When not .full, don't scroll. + interactionInProgress == false, // When interaction already in progress, don't scroll. + scrollView.frame.contains(point), // When point not in scrollView, don't scroll. + !grabberBarFrame.contains(point) // When point within grabber area, don't scroll. + else { + return false + } + + log.debug("ScrollView.contentOffset >>>", scrollView.contentOffset) + + if scrollView.contentOffset.y - scrollView.contentOffsetZero.y > 0 { + return true + } + if scrollView.isDecelerating { + return true + } + if velocity.y < 0 || velocity.y > 2500.0 { + return true + } + + return false + } + private func panningBegan() { // A user interaction does not always start from Began state of the pan gesture // because it can be recognized in scrolling a content in a content view controller. @@ -335,9 +358,6 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate private func startInteraction(with translation: CGPoint) { log.debug("startInteraction") initialFrame = surfaceView.frame - if let scrollView = scrollView { - initialScrollOffset = scrollView.contentOffset - } transOffsetY = translation.y viewcontroller.delegate?.floatingPanelWillBeginDragging(viewcontroller) diff --git a/Framework/Sources/GrabberHandleView.swift b/Framework/Sources/GrabberHandleView.swift index 214121c1..083c2faf 100644 --- a/Framework/Sources/GrabberHandleView.swift +++ b/Framework/Sources/GrabberHandleView.swift @@ -24,8 +24,14 @@ public class GrabberHandleView: UIView { self.backgroundColor = Default.barColor render() } + private func render() { self.layer.masksToBounds = true self.layer.cornerRadius = frame.size.height * 0.5 } + + public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let view = super.hitTest(point, with: event) + return view == self ? nil : view + } } From 77bbb9f715fd245cbc781217423760a4ff58bcc7 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Tue, 6 Nov 2018 10:04:32 +0900 Subject: [PATCH 048/623] Reset 'stopScrollDeceleration' on animation finish --- Framework/Sources/FloatingPanel.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Framework/Sources/FloatingPanel.swift b/Framework/Sources/FloatingPanel.swift index af8ff5d4..26ae655a 100644 --- a/Framework/Sources/FloatingPanel.swift +++ b/Framework/Sources/FloatingPanel.swift @@ -419,6 +419,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate self.animator = nil self.viewcontroller.delegate?.floatingPanelDidEndDecelerating(self.viewcontroller) + stopScrollDeceleration = false // Don't unlock scroll view in animating view when presentation layer != model layer unlockScrollView() } From a3bb5e5c1f1a3982b4a380ab2f01d2a65e7c3ad9 Mon Sep 17 00:00:00 2001 From: Florian Fittschen Date: Tue, 6 Nov 2018 10:01:57 +0100 Subject: [PATCH 049/623] Revert scrollview offset changes --- Framework/Sources/FloatingPanel.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Framework/Sources/FloatingPanel.swift b/Framework/Sources/FloatingPanel.swift index 620eddde..d6a4d15d 100644 --- a/Framework/Sources/FloatingPanel.swift +++ b/Framework/Sources/FloatingPanel.swift @@ -222,14 +222,11 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate switch panGesture { case scrollView?.panGestureRecognizer: guard let scrollView = scrollView else { return } - if panGesture.state == .began { - initialScrollOffset = scrollView.contentOffset - } if surfaceView.frame.minY > layoutAdapter.topY { switch state { case .full: // Prevent over scrolling from scroll top in moving the panel from full. - scrollView.contentOffset.y = initialScrollOffset.y + scrollView.contentOffset.y = scrollView.contentOffsetZero.y case .half, .tip: guard scrollView.isDecelerating == false else { // Don't fix the scroll offset in animating the panel to half and tip. @@ -358,6 +355,9 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate private func startInteraction(with translation: CGPoint) { log.debug("startInteraction") initialFrame = surfaceView.frame + if let scrollView = scrollView { + initialScrollOffset = scrollView.contentOffset + } transOffsetY = translation.y viewcontroller.delegate?.floatingPanelWillBeginDragging(viewcontroller) From 77a6b78bb1445a3375cfe91eb3102d1ee1bdcdf5 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Wed, 7 Nov 2018 09:35:34 +0900 Subject: [PATCH 050/623] Fix scrollview jumps after it moved programmatically --- Framework/Sources/FloatingPanel.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Framework/Sources/FloatingPanel.swift b/Framework/Sources/FloatingPanel.swift index c8a286a8..4ef91b04 100644 --- a/Framework/Sources/FloatingPanel.swift +++ b/Framework/Sources/FloatingPanel.swift @@ -642,6 +642,13 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate } } + func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { + if state != .full { + initialScrollOffset = scrollView.contentOffset + } + userScrollViewDelegate?.scrollViewDidEndScrollingAnimation?(scrollView) + } + func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { if stopScrollDeceleration { targetContentOffset.pointee = scrollView.contentOffset From 8c44ea77171acd7ee497e84465dd0297736bae2a Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Wed, 7 Nov 2018 10:44:35 +0900 Subject: [PATCH 051/623] Add 'Animate Scroll' button in Samples app --- Examples/Samples/Sources/ViewController.swift | 43 ++++++++++++++++--- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/Examples/Samples/Sources/ViewController.swift b/Examples/Samples/Sources/ViewController.swift index baee1da0..305b4de1 100644 --- a/Examples/Samples/Sources/ViewController.swift +++ b/Examples/Samples/Sources/ViewController.swift @@ -56,7 +56,7 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable tableView.delegate = self tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell") - let contentVC = DebugTableViewController(style: .plain) + let contentVC = DebugTableViewController() addMainPanel(with: contentVC) } @@ -111,7 +111,7 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let menu = Menu.allCases[indexPath.row] let contentVC: UIViewController = { - guard let storyboardID = menu.storyboardID else { return DebugTableViewController(style: .plain) } + guard let storyboardID = menu.storyboardID else { return DebugTableViewController() } guard let vc = self.storyboard?.instantiateViewController(withIdentifier: storyboardID) else { fatalError() } return vc }() @@ -217,16 +217,47 @@ class DebugTextViewController: UIViewController, UITextViewDelegate { } } -class DebugTableViewController: UITableViewController { +class DebugTableViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { + weak var tableView: UITableView! var items: [String] = [] override func viewDidLoad() { super.viewDidLoad() + + let tableView = UITableView(frame: .zero, + style: .plain) + view.addSubview(tableView) + tableView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + tableView.topAnchor.constraint(equalTo: view.topAnchor), + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + tableView.leftAnchor.constraint(equalTo: view.leftAnchor), + tableView.rightAnchor.constraint(equalTo: view.rightAnchor) + ]) + tableView.dataSource = self + tableView.delegate = self + self.tableView = tableView + + let button = UIButton() + button.setTitle("Animate Scroll", for: .normal) + button.setTitleColor(view.tintColor, for: .normal) + button.addTarget(self, action: #selector(doScrollAnimate), for: .touchUpInside) + view.addSubview(button) + button.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + button.topAnchor.constraint(equalTo: view.topAnchor, constant: 22.0), + button.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -22.0), + ]) + for i in 0...100 { items.append("Items \(i)") } tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell") } + @objc func doScrollAnimate() { + tableView.scrollToRow(at: IndexPath(row: 50, section: 0), at: .top, animated: true) + } + @objc func close(sender: UIButton) { // Remove FloatingPanel from a view (self.parent as! FloatingPanelController).removePanelFromParent(animated: true, completion: nil) @@ -276,15 +307,15 @@ class DebugTableViewController: UITableViewController { print("Content View: willTransition(to: \(newCollection), with: \(coordinator))") } - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return items.count } - override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return 66.0 } - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) cell.textLabel?.text = items[indexPath.row] return cell From 6bceb6e9d6f2a26786386d92e42686889ad91287 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Wed, 7 Nov 2018 10:45:32 +0900 Subject: [PATCH 052/623] Fix the surface height on orientation change --- Framework/Sources/FloatingPanelLayout.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Framework/Sources/FloatingPanelLayout.swift b/Framework/Sources/FloatingPanelLayout.swift index d0bed104..ebcb13d3 100644 --- a/Framework/Sources/FloatingPanelLayout.swift +++ b/Framework/Sources/FloatingPanelLayout.swift @@ -96,6 +96,7 @@ public class FloatingPanelDefaultLandscapeLayout: FloatingPanelLayout { class FloatingPanelLayoutAdapter { + private weak var parent: UIViewController! private weak var surfaceView: FloatingPanelSurfaceView! private weak var backdropVIew: FloatingPanelBackdropView! @@ -177,7 +178,7 @@ class FloatingPanelLayoutAdapter { } func prepareLayout(toParent parent: UIViewController) { - parentHeight = parent.view.frame.height + self.parent = parent surfaceView.translatesAutoresizingMaskIntoConstraints = false backdropVIew.translatesAutoresizingMaskIntoConstraints = false @@ -226,7 +227,7 @@ class FloatingPanelLayoutAdapter { } NSLayoutConstraint.deactivate(heightConstraints) - let height = parentHeight - (safeAreaInsets.top + fullInset) + let height = self.parent.view.bounds.height - (safeAreaInsets.top + fullInset) heightConstraints = [ surfaceView.heightAnchor.constraint(equalToConstant: height) ] From f8e9af563019112db3a3e3637fa9f76c38a35efb Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Wed, 7 Nov 2018 10:54:28 +0900 Subject: [PATCH 053/623] Improve removal interaction impl --- Framework/Sources/FloatingPanel.swift | 52 ++++++++++++++------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/Framework/Sources/FloatingPanel.swift b/Framework/Sources/FloatingPanel.swift index c8a286a8..451c6661 100644 --- a/Framework/Sources/FloatingPanel.swift +++ b/Framework/Sources/FloatingPanel.swift @@ -332,31 +332,8 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate endInteraction(for: targetPosition) if isRemovalInteractionEnabled, isBottomState { - let posY = layoutAdapter.positionY(for: state) - let currentY = getCurrentY(from: initialFrame, with: translation) - let safeAreaBottomY = layoutAdapter.safeAreaBottomY - let vth = behavior.removalVelocityThreshold - let velocityVector = (distance != 0) ? CGVector(dx: 0, dy: max(min(velocity.y/distance, vth), 0.0)) : .zero - - let startRemovalAnimation = { - let animator = self.behavior.removalInteractionAnimator(self.viewcontroller, with: velocityVector) - animator.addAnimations { [weak self] in - guard let self = self else { return } - self.updateLayout(to: nil) - } - animator.addCompletion({ [weak self] (_) in - guard let self = self else { return } - self.viewcontroller.delegate?.floatingPanelDidEndRemove(self.viewcontroller) - }) - animator.startAnimation() - } - - if (currentY - posY) != 0 { - if (safeAreaBottomY - posY) / (currentY - posY) >= 0.5 || velocityVector.dy == vth { - viewcontroller.delegate?.floatingPanelDidEndDraggingToRemove(viewcontroller, withVelocity: velocity) - startRemovalAnimation() - return - } + if startRemovalAnimation(with: translation, velocity: velocity, distance: distance) { + return } } @@ -366,6 +343,31 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate startAnimation(to: targetPosition, at: distance, with: velocity) } + private func startRemovalAnimation(with translation: CGPoint, velocity: CGPoint, distance: CGFloat) -> Bool { + let posY = layoutAdapter.positionY(for: state) + let currentY = getCurrentY(from: initialFrame, with: translation) + let safeAreaBottomY = layoutAdapter.safeAreaBottomY + let vth = behavior.removalVelocityThreshold + let velocityVector = (distance != 0) ? CGVector(dx: 0, dy: max(min(velocity.y/distance, vth), 0.0)) : .zero + + guard (currentY - posY) != 0 else { return false } + guard (safeAreaBottomY - posY) / (currentY - posY) >= 0.5 || velocityVector.dy == vth else { return false } + + viewcontroller.delegate?.floatingPanelDidEndDraggingToRemove(viewcontroller, withVelocity: velocity) + let animator = self.behavior.removalInteractionAnimator(self.viewcontroller, with: velocityVector) + animator.addAnimations { [weak self] in + guard let self = self else { return } + self.updateLayout(to: nil) + } + animator.addCompletion({ [weak self] (_) in + guard let self = self else { return } + self.viewcontroller.removePanelFromParent(animated: false) + self.viewcontroller.delegate?.floatingPanelDidEndRemove(self.viewcontroller) + }) + animator.startAnimation() + return true + } + private func startInteraction(with translation: CGPoint) { log.debug("startInteraction") initialFrame = surfaceView.frame From d0f5d1bd0b226f3951678c6e5476e13a6e05e700 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Wed, 7 Nov 2018 11:06:18 +0900 Subject: [PATCH 054/623] Fix backdrop handling --- Framework/Sources/FloatingPanel.swift | 11 +--------- Framework/Sources/FloatingPanelLayout.swift | 23 ++++++++++++++------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Framework/Sources/FloatingPanel.swift b/Framework/Sources/FloatingPanel.swift index 451c6661..325002d0 100644 --- a/Framework/Sources/FloatingPanel.swift +++ b/Framework/Sources/FloatingPanel.swift @@ -145,15 +145,6 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate private func updateLayout(to target: FloatingPanelPosition?) { self.layoutAdapter.activateLayout(of: target) - self.setBackdropAlpha(of: target) - } - - private func setBackdropAlpha(of target: FloatingPanelPosition?) { - if let target = target { - self.backdropView.alpha = layoutAdapter.layout.backdropAlphaFor(position: target) - } else { - self.backdropView.alpha = 0.0 - } } private func getBackdropAlpha(with translation: CGPoint) -> CGFloat { @@ -417,7 +408,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate guard let self = self else { return } if self.state == targetPosition { self.surfaceView.frame.origin.y = targetY - self.setBackdropAlpha(of: targetPosition) + self.layoutAdapter.setBackdropAlpha(of: targetPosition) } else { self.updateLayout(to: targetPosition) } diff --git a/Framework/Sources/FloatingPanelLayout.swift b/Framework/Sources/FloatingPanelLayout.swift index ebcb13d3..3e6de75f 100644 --- a/Framework/Sources/FloatingPanelLayout.swift +++ b/Framework/Sources/FloatingPanelLayout.swift @@ -98,7 +98,7 @@ public class FloatingPanelDefaultLandscapeLayout: FloatingPanelLayout { class FloatingPanelLayoutAdapter { private weak var parent: UIViewController! private weak var surfaceView: FloatingPanelSurfaceView! - private weak var backdropVIew: FloatingPanelBackdropView! + private weak var backdropView: FloatingPanelBackdropView! var layout: FloatingPanelLayout { didSet { checkConsistance(of: layout) } @@ -174,27 +174,27 @@ class FloatingPanelLayoutAdapter { init(surfaceView: FloatingPanelSurfaceView, backdropView: FloatingPanelBackdropView, layout: FloatingPanelLayout) { self.layout = layout self.surfaceView = surfaceView - self.backdropVIew = backdropView + self.backdropView = backdropView } func prepareLayout(toParent parent: UIViewController) { self.parent = parent surfaceView.translatesAutoresizingMaskIntoConstraints = false - backdropVIew.translatesAutoresizingMaskIntoConstraints = false + backdropView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.deactivate(fixedConstraints + fullConstraints + halfConstraints + tipConstraints + offConstraints) // Fixed constraints of surface and backdrop views let surfaceConstraints = layout.prepareLayout(surfaceView: surfaceView, in: parent.view!) let backdroptConstraints = [ - backdropVIew.topAnchor.constraint(equalTo: parent.view.topAnchor, + backdropView.topAnchor.constraint(equalTo: parent.view.topAnchor, constant: 0.0), - backdropVIew.leftAnchor.constraint(equalTo: parent.view.leftAnchor, + backdropView.leftAnchor.constraint(equalTo: parent.view.leftAnchor, constant: 0.0), - backdropVIew.rightAnchor.constraint(equalTo: parent.view.rightAnchor, + backdropView.rightAnchor.constraint(equalTo: parent.view.rightAnchor, constant: 0.0), - backdropVIew.bottomAnchor.constraint(equalTo: parent.view.bottomAnchor, + backdropView.bottomAnchor.constraint(equalTo: parent.view.bottomAnchor, constant: 0.0), ] fixedConstraints = surfaceConstraints + backdroptConstraints @@ -239,6 +239,7 @@ class FloatingPanelLayoutAdapter { defer { surfaceView.superview!.layoutIfNeeded() } + setBackdropAlpha(of: state) NSLayoutConstraint.activate(fixedConstraints) @@ -266,6 +267,14 @@ class FloatingPanelLayoutAdapter { } } + func setBackdropAlpha(of target: FloatingPanelPosition?) { + if let target = target { + self.backdropView.alpha = layout.backdropAlphaFor(position: target) + } else { + self.backdropView.alpha = 0.0 + } + } + private func checkConsistance(of layout: FloatingPanelLayout) { // Verify layout configurations let supportedPositions = layout.supportedPositions From 27bf224206b3d2c903a2baa489ce29ce4ac42b6a Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Wed, 7 Nov 2018 11:11:51 +0900 Subject: [PATCH 055/623] Modify the backdrop default behavior --- Examples/Maps/Maps/ViewController.swift | 4 ++++ Framework/Sources/FloatingPanelController.swift | 1 - Framework/Sources/FloatingPanelLayout.swift | 7 ------- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/Examples/Maps/Maps/ViewController.swift b/Examples/Maps/Maps/ViewController.swift index 97c2d1d7..3ac60b92 100644 --- a/Examples/Maps/Maps/ViewController.swift +++ b/Examples/Maps/Maps/ViewController.swift @@ -234,6 +234,10 @@ public class SearchPanelLandscapeLayout: FloatingPanelLayout { ] } } + + public func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat { + return 0.0 + } } class SearchCell: UITableViewCell { diff --git a/Framework/Sources/FloatingPanelController.swift b/Framework/Sources/FloatingPanelController.swift index 08fd21c7..9072d414 100644 --- a/Framework/Sources/FloatingPanelController.swift +++ b/Framework/Sources/FloatingPanelController.swift @@ -162,7 +162,6 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI if let parent = parent { self.update(safeAreaInsets: parent.layoutInsets) } - floatingPanel.backdropView.isHidden = (traitCollection.verticalSizeClass == .compact) } public override func viewDidAppear(_ animated: Bool) { diff --git a/Framework/Sources/FloatingPanelLayout.swift b/Framework/Sources/FloatingPanelLayout.swift index 3e6de75f..ea4f6ce9 100644 --- a/Framework/Sources/FloatingPanelLayout.swift +++ b/Framework/Sources/FloatingPanelLayout.swift @@ -85,13 +85,6 @@ public class FloatingPanelDefaultLandscapeLayout: FloatingPanelLayout { default: return nil } } - - public func prepareLayout(surfaceView: UIView, in view: UIView) -> [NSLayoutConstraint] { - return [ - surfaceView.leftAnchor.constraint(equalTo: view.sideLayoutGuide.leftAnchor, constant: 0.0), - surfaceView.rightAnchor.constraint(equalTo: view.sideLayoutGuide.rightAnchor, constant: 0.0), - ] - } } From f68b094b80705bdc3ccad8d231c8bd763cf6a403 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Wed, 7 Nov 2018 11:36:51 +0900 Subject: [PATCH 056/623] Fix removal interaction --- Framework/Sources/FloatingPanel.swift | 7 ++++--- Framework/Sources/FloatingPanelBehavior.swift | 17 +++++++++++++---- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/Framework/Sources/FloatingPanel.swift b/Framework/Sources/FloatingPanel.swift index 325002d0..1abf1f02 100644 --- a/Framework/Sources/FloatingPanel.swift +++ b/Framework/Sources/FloatingPanel.swift @@ -338,11 +338,12 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate let posY = layoutAdapter.positionY(for: state) let currentY = getCurrentY(from: initialFrame, with: translation) let safeAreaBottomY = layoutAdapter.safeAreaBottomY - let vth = behavior.removalVelocityThreshold + let vth = behavior.removalVelocity + let pth = max(min(behavior.removalProgress, 1.0), 0.0) let velocityVector = (distance != 0) ? CGVector(dx: 0, dy: max(min(velocity.y/distance, vth), 0.0)) : .zero - guard (currentY - posY) != 0 else { return false } - guard (safeAreaBottomY - posY) / (currentY - posY) >= 0.5 || velocityVector.dy == vth else { return false } + guard (safeAreaBottomY - posY) != 0 else { return false } + guard (currentY - posY) / (safeAreaBottomY - posY) >= pth || velocityVector.dy == vth else { return false } viewcontroller.delegate?.floatingPanelDidEndDraggingToRemove(viewcontroller, withVelocity: velocity) let animator = self.behavior.removalInteractionAnimator(self.viewcontroller, with: velocityVector) diff --git a/Framework/Sources/FloatingPanelBehavior.swift b/Framework/Sources/FloatingPanelBehavior.swift index 32c3dbbf..0b13b302 100644 --- a/Framework/Sources/FloatingPanelBehavior.swift +++ b/Framework/Sources/FloatingPanelBehavior.swift @@ -29,12 +29,17 @@ public protocol FloatingPanelBehavior { /// Returns a y-axis velocity to invoke a removal interaction at the bottom position. /// - /// This method is called when FloatingPanelController.isRemovalInteractionEnabled is true. - var removalVelocityThreshold: CGFloat { get } + /// Default is 10.0. This method is called when FloatingPanelController.isRemovalInteractionEnabled is true. + var removalVelocity: CGFloat { get } + + /// Returns the threshold of the transition to invoke a removal interaction at the bottom position. + /// + /// The progress is represented by a floating-point value between 0.0 and 1.0, inclusive, where 1.0 indicates the floating panel is impossible to invoke the removal interaction. The default value is 0.5. Values less than 0.0 and greater than 1.0 are pinned to those limits. This method is called when FloatingPanelController.isRemovalInteractionEnabled is true. + var removalProgress: CGFloat { get } /// Returns a UIViewPropertyAnimator object to remove a floating panel with a velocity interactively at the bottom position. /// - /// This method is called when FloatingPanelController.isRemovalInteractionEnabled is true. + /// Default is a spring animator with 1.0 damping ratio. This method is called when FloatingPanelController.isRemovalInteractionEnabled is true. func removalInteractionAnimator(_ fpc: FloatingPanelController, with velocity: CGVector) -> UIViewPropertyAnimator } @@ -51,10 +56,14 @@ public extension FloatingPanelBehavior { return UIViewPropertyAnimator(duration: 0.25, curve: .easeInOut) } - var removalVelocityThreshold: CGFloat { + var removalVelocity: CGFloat { return 10.0 } + var removalProgress: CGFloat { + return 0.5 + } + func removalInteractionAnimator(_ fpc: FloatingPanelController, with velocity: CGVector) -> UIViewPropertyAnimator { log.debug("velocity", velocity) let timing = UISpringTimingParameters(dampingRatio: 1.0, From 74af35af9057937d46a8cb65a9799e0e12836ec1 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Wed, 7 Nov 2018 11:47:22 +0900 Subject: [PATCH 057/623] Introduce FloatingPanelBehavior.redirectionalProgress(_:from:to:) --- Framework/Sources/FloatingPanel.swift | 41 +++++++++++++++++-- Framework/Sources/FloatingPanelBehavior.swift | 11 ++++- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/Framework/Sources/FloatingPanel.swift b/Framework/Sources/FloatingPanel.swift index 1abf1f02..d2d29670 100644 --- a/Framework/Sources/FloatingPanel.swift +++ b/Framework/Sources/FloatingPanel.swift @@ -537,14 +537,44 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate return targetPosition(from: [.full, .tip], at: currentY, velocity: velocity) default: /* - [topY|full]---[th1]---[middleY|default]---[th2]---[bottomY|collapsed] + [topY|full]---[th1]---[middleY|half]---[th2]---[bottomY|tip] */ let topY = layoutAdapter.topY let middleY = layoutAdapter.middleY let bottomY = layoutAdapter.bottomY - let th1 = (topY + middleY) / 2 - let th2 = (middleY + bottomY) / 2 + let target: FloatingPanelPosition + let forwardYDirection: Bool + + switch state { + case .full: + target = .half + forwardYDirection = true + case .half: + if (currentY < middleY) { + target = .full + forwardYDirection = false + } else { + target = .tip + forwardYDirection = true + } + case .tip: + target = .half + forwardYDirection = false + } + + let redirectionalProgress = max(min(behavior.redirectionalProgress(viewcontroller, from: state, to: target), 1.0), 0.0) + + let th1: CGFloat + let th2: CGFloat + + if forwardYDirection { + th1 = topY + (middleY - topY) * redirectionalProgress + th2 = middleY + (bottomY - middleY) * redirectionalProgress + } else { + th1 = middleY - (middleY - topY) * redirectionalProgress + th2 = bottomY - (bottomY - middleY) * redirectionalProgress + } switch currentY { case .. CGFloat + + /// Returns a UIViewPropertyAnimator object to project a floating panel to a position on finger up if the user dragged. func interactionAnimator(_ fpc: FloatingPanelController, to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator /// Returns a UIViewPropertyAnimator object to add a floating panel to a position. @@ -44,6 +49,10 @@ public protocol FloatingPanelBehavior { } public extension FloatingPanelBehavior { + func redirectionalProgress(_ fpc: FloatingPanelController, from: FloatingPanelPosition, to: FloatingPanelPosition) -> CGFloat { + return 0.5 + } + func addAnimator(_ fpc: FloatingPanelController, to: FloatingPanelPosition) -> UIViewPropertyAnimator { return UIViewPropertyAnimator(duration: 0.25, curve: .easeInOut) } From fcd71dc2c8e873c5d69923f6c7e5e5a0a8f920c0 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Wed, 7 Nov 2018 11:50:15 +0900 Subject: [PATCH 058/623] Add RemovablePanelLandscapeLayout --- Examples/Samples/Sources/ViewController.swift | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/Examples/Samples/Sources/ViewController.swift b/Examples/Samples/Sources/ViewController.swift index baee1da0..d3103261 100644 --- a/Examples/Samples/Sources/ViewController.swift +++ b/Examples/Samples/Sources/ViewController.swift @@ -147,7 +147,7 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? { if currentMenu == .showRemovablePanel { - return RemovablePanelLayout() + return newCollection.verticalSizeClass == .compact ? RemovablePanelLandscapeLayout() : RemovablePanelLayout() } else { return nil } @@ -177,6 +177,28 @@ class RemovablePanelLayout: FloatingPanelLayout { } } +class RemovablePanelLandscapeLayout: FloatingPanelLayout { + var initialPosition: FloatingPanelPosition { + return .half + } + var supportedPositions: Set { + return [.half] + } + var bottomInteractionBuffer: CGFloat { + return 261.0 - 22.0 + } + + func insetFor(position: FloatingPanelPosition) -> CGFloat? { + switch position { + case .half: return 261.0 + default: return nil + } + } + func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat { + return 0.3 + } +} + class NestedScrollViewController: UIViewController { @IBOutlet weak var scrollView: UIScrollView! From 7755da6203b02c93478324b6dd12c5f9ef932d19 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Thu, 8 Nov 2018 09:51:31 +0900 Subject: [PATCH 059/623] Update checkLayoutConsistance --- Framework/Sources/FloatingPanelLayout.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Framework/Sources/FloatingPanelLayout.swift b/Framework/Sources/FloatingPanelLayout.swift index ea4f6ce9..6c0910e6 100644 --- a/Framework/Sources/FloatingPanelLayout.swift +++ b/Framework/Sources/FloatingPanelLayout.swift @@ -94,12 +94,15 @@ class FloatingPanelLayoutAdapter { private weak var backdropView: FloatingPanelBackdropView! var layout: FloatingPanelLayout { - didSet { checkConsistance(of: layout) } + didSet { + checkLayoutConsistance() + } } var safeAreaInsets: UIEdgeInsets = .zero { didSet { updateHeight() + checkLayoutConsistance() } } @@ -268,7 +271,7 @@ class FloatingPanelLayoutAdapter { } } - private func checkConsistance(of layout: FloatingPanelLayout) { + func checkLayoutConsistance() { // Verify layout configurations let supportedPositions = layout.supportedPositions From b5f7fbc1a50e634b68744648eaebcb34bc674b49 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Wed, 7 Nov 2018 18:03:31 +0900 Subject: [PATCH 060/623] Fix scroll top bounce bugs Disable scroll top bouncing if a user scroll down contents(no deceleration) and the scroll offset Y is less than 10.0, instead of the velocity condition(greater than 2500.0). This change prevents potential bugs on scroll bouncing so that the scroll view tracking is more robust. --- Framework/Sources/FloatingPanel.swift | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/Framework/Sources/FloatingPanel.swift b/Framework/Sources/FloatingPanel.swift index 4ef91b04..77a49e3a 100644 --- a/Framework/Sources/FloatingPanel.swift +++ b/Framework/Sources/FloatingPanel.swift @@ -20,7 +20,6 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate didSet { guard let scrollView = scrollView else { return } scrollView.panGestureRecognizer.addTarget(self, action: #selector(handle(panGesture:))) - scrollBouncable = scrollView.bounces scrollIndictorVisible = scrollView.showsVerticalScrollIndicator } } @@ -50,7 +49,6 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate // Scroll handling private var stopScrollDeceleration: Bool = false - private var scrollBouncable = false private var scrollIndictorVisible = false // MARK: - Interface @@ -222,6 +220,14 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate switch panGesture { case scrollView?.panGestureRecognizer: guard let scrollView = scrollView else { return } + + log.debug("SrollPanGesture ScrollView.contentOffset >>>", scrollView.contentOffset.y) + + // Prevent scoll slip by the top bounce + if scrollView.isDecelerating == false { + scrollView.bounces = (scrollView.contentOffset.y > 10.0) + } + if surfaceView.frame.minY > layoutAdapter.topY { switch state { case .full: @@ -243,7 +249,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate let velocity = panGesture.velocity(in: panGesture.view) let location = panGesture.location(in: panGesture.view) - log.debug(panGesture.state, ">>>", "{ translation: \(translation), velocity: \(velocity) }") + log.debug(panGesture.state, ">>>", "translation: \(translation.y), velocity: \(velocity.y)") if shouldScrollViewHandleTouch(scrollView, point: location, velocity: velocity) { return @@ -283,15 +289,15 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate return false } - log.debug("ScrollView.contentOffset >>>", scrollView.contentOffset) + log.debug("ScrollView.contentOffset >>>", scrollView.contentOffset.y) - if scrollView.contentOffset.y - scrollView.contentOffsetZero.y > 0 { + if scrollView.contentOffset.y - scrollView.contentOffsetZero.y != 0 { return true } if scrollView.isDecelerating { return true } - if velocity.y < 0 || velocity.y > 2500.0 { + if velocity.y < 0 { return true } @@ -383,7 +389,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate private func endInteraction(for targetPosition: FloatingPanelPosition) { log.debug("endInteraction for \(targetPosition)") if targetPosition != .full { - lockScrollView(withBounce: true) + lockScrollView() } interactionInProgress = false } @@ -610,13 +616,10 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate // MARK: - ScrollView handling - func lockScrollView(withBounce bounce: Bool = false) { + func lockScrollView() { guard let scrollView = scrollView else { return } scrollView.isDirectionalLockEnabled = true - if bounce { - scrollView.bounces = false - } scrollView.showsVerticalScrollIndicator = false } @@ -624,7 +627,6 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate guard let scrollView = scrollView else { return } scrollView.isDirectionalLockEnabled = false - scrollView.bounces = scrollBouncable scrollView.showsVerticalScrollIndicator = scrollIndictorVisible } From 12c8369e86efc94cfe310fc3ecd1e94bff6a53be Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Fri, 9 Nov 2018 15:02:50 +0900 Subject: [PATCH 061/623] Add FloatingPanelController.updateLayout() --- Framework/Sources/FloatingPanel.swift | 2 +- .../Sources/FloatingPanelController.swift | 46 +++++++++++++++---- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/Framework/Sources/FloatingPanel.swift b/Framework/Sources/FloatingPanel.swift index 8ace0c34..b3f52e67 100644 --- a/Framework/Sources/FloatingPanel.swift +++ b/Framework/Sources/FloatingPanel.swift @@ -78,7 +78,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate panGesture.delegate = self } - func layoutViews(in vc: UIViewController) { + func setUpViews(in vc: UIViewController) { unowned let view = vc.view! view.insertSubview(backdropView, belowSubview: surfaceView) diff --git a/Framework/Sources/FloatingPanelController.swift b/Framework/Sources/FloatingPanelController.swift index 9072d414..ebba2b4c 100644 --- a/Framework/Sources/FloatingPanelController.swift +++ b/Framework/Sources/FloatingPanelController.swift @@ -89,6 +89,16 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI return floatingPanel.state } + /// The layout object managed by the controller + public var layout: FloatingPanelLayout { + return floatingPanel.layoutAdapter.layout + } + + /// The behavior object managed by the controller + public var behavior: FloatingPanelBehavior { + return floatingPanel.behavior + } + /// The content insets of the tracking scroll view derived from the safe area of the parent view public var adjustedContentInsets: UIEdgeInsets { return floatingPanel.layoutAdapter.adjustedContentInsets @@ -146,13 +156,9 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI super.willTransition(to: newCollection, with: coordinator) // Change layout for a new trait collection - floatingPanel.layoutAdapter.layout = fetchLayout(for: newCollection) - floatingPanel.behavior = fetchBehavior(for: newCollection) - - guard let parent = parent else { fatalError() } + updateLayout(for: newCollection) - floatingPanel.layoutAdapter.prepareLayout(toParent: parent) - floatingPanel.layoutAdapter.activateLayout(of: floatingPanel.state) + floatingPanel.behavior = fetchBehavior(for: newCollection) } public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { @@ -199,6 +205,17 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI } } + private func updateLayout(for: UITraitCollection) { + floatingPanel.layoutAdapter.layout = fetchLayout(for: view.traitCollection) + + assert(floatingPanel.layoutAdapter.layout.supportedPositions.contains(floatingPanel.state)) + + guard let parent = parent else { return } + + floatingPanel.layoutAdapter.prepareLayout(toParent: parent) + floatingPanel.layoutAdapter.activateLayout(of: floatingPanel.state) + } + // MARK: - Container view controller interface /// Adds the view managed by the controller as a child of the specified view controller. @@ -243,10 +260,10 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI // 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.layoutViews(in: parent) - floatingPanel.behavior = fetchBehavior(for: traitCollection) + floatingPanel.setUpViews(in: parent) + floatingPanel.present(animated: animated) { [weak self] in guard let self = self else { return } self.didMove(toParent: parent) @@ -341,7 +358,18 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI } } - // MARK: - Helpers + // MARK: - Utilities + + /// Updates the layout object from the delegate and lays out the views managed + /// by the controller immediately. + /// + /// This method updates the `FloatingPanelLayout` object from the delegate and + /// then it calls `layoutIfNeeded()` of the parent's root view to force the view + /// to update the floating panel's layout immediately. It can be called in an + /// animation block. + public func updateLayout() { + updateLayout(for: view.traitCollection) + } /// Returns the y-coordinate of the point at the origin of the surface view public func originYOfSurface(for pos: FloatingPanelPosition) -> CGFloat { From d96df74412e869203f59c61e5c244454a21448c9 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Mon, 12 Nov 2018 11:49:19 +0900 Subject: [PATCH 062/623] Add update layout sample --- .../Sources/Base.lproj/Main.storyboard | 9 +++++- Examples/Samples/Sources/ViewController.swift | 32 ++++++++++++++++++- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/Examples/Samples/Sources/Base.lproj/Main.storyboard b/Examples/Samples/Sources/Base.lproj/Main.storyboard index d9dd2765..ed108d90 100644 --- a/Examples/Samples/Sources/Base.lproj/Main.storyboard +++ b/Examples/Samples/Sources/Base.lproj/Main.storyboard @@ -190,7 +190,7 @@ - + + diff --git a/Examples/Samples/Sources/ViewController.swift b/Examples/Samples/Sources/ViewController.swift index f8699750..411b16c9 100644 --- a/Examples/Samples/Sources/ViewController.swift +++ b/Examples/Samples/Sources/ViewController.swift @@ -374,14 +374,19 @@ class DetailViewController: UIViewController { } } -class ModalViewController: UIViewController { +class ModalViewController: UIViewController, FloatingPanelControllerDelegate { var fpc: FloatingPanelController! var consoleVC: DebugTextViewController! + @IBOutlet weak var safeAreaView: UIView! + + var isNewlayout: Bool = false + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) // Initialize FloatingPanelController fpc = FloatingPanelController() + fpc.delegate = self // Initialize FloatingPanelController and add the view fpc.surfaceView.cornerRadius = 6.0 @@ -417,6 +422,31 @@ class ModalViewController: UIViewController { @IBAction func moveToTip(sender: UIButton) { fpc.move(to: .tip, animated: true) } + + @IBAction func updateLayout(_ sender: Any) { + isNewlayout = !isNewlayout + UIView.animate(withDuration: 0.5) { + self.fpc.updateLayout() + } + } + + func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? { + return (isNewlayout) ? ModalSecondLayout() : nil + } +} + +class ModalSecondLayout: FloatingPanelLayout { + var initialPosition: FloatingPanelPosition { + return .half + } + + func insetFor(position: FloatingPanelPosition) -> CGFloat? { + switch position { + case .full: return 18.0 + case .half: return 262.0 + case .tip: return 44.0 + } + } } class TabBarViewController: UITabBarController {} From 7ba8d3b9f6f7b253950a9c099b2e6c249b7a1caf Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Mon, 12 Nov 2018 11:50:44 +0900 Subject: [PATCH 063/623] Fix FloatingPanel's initial state value --- Framework/Sources/FloatingPanel.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Framework/Sources/FloatingPanel.swift b/Framework/Sources/FloatingPanel.swift index b3f52e67..784548a7 100644 --- a/Framework/Sources/FloatingPanel.swift +++ b/Framework/Sources/FloatingPanel.swift @@ -65,6 +65,8 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate layout: layout) self.behavior = behavior + state = layoutAdapter.layout.initialPosition + panGesture = FloatingPanelPanGestureRecognizer() if #available(iOS 11.0, *) { From f3ada5fa5f6632d25865ac0fff5eca6ee0294dcd Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Mon, 12 Nov 2018 10:50:45 +0900 Subject: [PATCH 064/623] Fix scroll view indicators hidden at the top once --- Framework/Sources/FloatingPanel.swift | 32 +++++++++++++++++++++------ 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/Framework/Sources/FloatingPanel.swift b/Framework/Sources/FloatingPanel.swift index 784548a7..a261a888 100644 --- a/Framework/Sources/FloatingPanel.swift +++ b/Framework/Sources/FloatingPanel.swift @@ -236,6 +236,16 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate // Fix the scroll offset in moving the panel from half and tip. scrollView.contentOffset.y = initialScrollOffset.y } + + // Always hide a scroll indicator at the non-top. + if interactionInProgress { + lockScrollView() + } + } else { + // Always show a scroll indicator at the top. + if interactionInProgress { + unlockScrollView() + } } case panGesture: let translation = panGesture.translation(in: panGesture.view!.superview) @@ -369,25 +379,27 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate } private func startInteraction(with translation: CGPoint) { + /* Don't lock a scroll view to show a scroll indicator after hitting the top */ log.debug("startInteraction") initialFrame = surfaceView.frame if let scrollView = scrollView { initialScrollOffset = scrollView.contentOffset } transOffsetY = translation.y - viewcontroller.delegate?.floatingPanelWillBeginDragging(viewcontroller) - lockScrollView() + viewcontroller.delegate?.floatingPanelWillBeginDragging(viewcontroller) interactionInProgress = true } private func endInteraction(for targetPosition: FloatingPanelPosition) { log.debug("endInteraction for \(targetPosition)") + interactionInProgress = false + + // Prevent to keep a scoll view indicator visible at the half/tip position if targetPosition != .full { lockScrollView() } - interactionInProgress = false } private func getCurrentY(from rect: CGRect, with translation: CGPoint) -> CGFloat { @@ -645,15 +657,21 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate // MARK: - ScrollView handling - func lockScrollView() { - guard let scrollView = scrollView else { return } + private func lockScrollView() { + guard let scrollView = scrollView, + scrollView.isDirectionalLockEnabled == false, + scrollView.showsVerticalScrollIndicator == true + else { return } scrollView.isDirectionalLockEnabled = true scrollView.showsVerticalScrollIndicator = false } - func unlockScrollView() { - guard let scrollView = scrollView else { return } + private func unlockScrollView() { + guard let scrollView = scrollView, + scrollView.isDirectionalLockEnabled == true, + scrollView.showsVerticalScrollIndicator != scrollIndictorVisible + else { return } scrollView.isDirectionalLockEnabled = false scrollView.showsVerticalScrollIndicator = scrollIndictorVisible From aa6cd3421e0dc879bbe84bd4c0b954d7b2d7a89a Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Mon, 12 Nov 2018 11:09:37 +0900 Subject: [PATCH 065/623] Add comments and fix typos --- Framework/Sources/FloatingPanelController.swift | 12 ++++++++---- Framework/Sources/FloatingPanelLayout.swift | 8 ++++++-- Framework/Sources/UIExtensions.swift | 2 ++ 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/Framework/Sources/FloatingPanelController.swift b/Framework/Sources/FloatingPanelController.swift index ebba2b4c..c47c3a52 100644 --- a/Framework/Sources/FloatingPanelController.swift +++ b/Framework/Sources/FloatingPanelController.swift @@ -243,9 +243,12 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI layoutInsetsObservations.removeAll() - // Must track safeAreaInsets/{top,bottom}LayoutGuide of the `parent.view` to update floatingPanel.safeAreaInsets`. - // Because the parent VC does not call viewSafeAreaInsetsDidChange() expectedly on the bottom inset's update. - // So I needs to observe them. It ensures that the `adjustedContentInsets` has a correct value. + // Must track safeAreaInsets/{top,bottom}LayoutGuide of the `parent.view` + // to update floatingPanel.safeAreaInsets`. There are 2 reasons. + // 1. 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 = parent.observe(\.view.safeAreaInsets) { [weak self] (vc, chaneg) in guard let self = self else { return } @@ -253,7 +256,8 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI } layoutInsetsObservations.append(observaion) } else { - // KVOs for topLayoutGuide & bottomLayoutGuide are not effective. Instead, safeAreaInsets will be updated in viewDidAppear() + // KVOs for topLayoutGuide & bottomLayoutGuide are not effective. + // Instead, safeAreaInsets will be updated in viewDidAppear() } parent.addChild(self) diff --git a/Framework/Sources/FloatingPanelLayout.swift b/Framework/Sources/FloatingPanelLayout.swift index 6c0910e6..b2d9443c 100644 --- a/Framework/Sources/FloatingPanelLayout.swift +++ b/Framework/Sources/FloatingPanelLayout.swift @@ -183,7 +183,7 @@ class FloatingPanelLayoutAdapter { // Fixed constraints of surface and backdrop views let surfaceConstraints = layout.prepareLayout(surfaceView: surfaceView, in: parent.view!) - let backdroptConstraints = [ + let backdropConstraints = [ backdropView.topAnchor.constraint(equalTo: parent.view.topAnchor, constant: 0.0), backdropView.leftAnchor.constraint(equalTo: parent.view.leftAnchor, @@ -193,7 +193,7 @@ class FloatingPanelLayoutAdapter { backdropView.bottomAnchor.constraint(equalTo: parent.view.bottomAnchor, constant: 0.0), ] - fixedConstraints = surfaceConstraints + backdroptConstraints + fixedConstraints = surfaceConstraints + backdropConstraints // Flexible surface constarints for full, half, tip and off fullConstraints = [ @@ -223,6 +223,10 @@ class FloatingPanelLayoutAdapter { } NSLayoutConstraint.deactivate(heightConstraints) + // Must use the parent height, not the screen height because safe area insets + // of the parent are relative values. For example, a view controller in + // Navigation controller's safe area insets and frame can be changed whether + // the navigation bar is translucent or not. let height = self.parent.view.bounds.height - (safeAreaInsets.top + fullInset) heightConstraints = [ surfaceView.heightAnchor.constraint(equalToConstant: height) diff --git a/Framework/Sources/UIExtensions.swift b/Framework/Sources/UIExtensions.swift index 50084230..23da0974 100644 --- a/Framework/Sources/UIExtensions.swift +++ b/Framework/Sources/UIExtensions.swift @@ -50,6 +50,8 @@ protocol SideLayoutGuideProvider { extension UIView: SideLayoutGuideProvider {} extension UILayoutGuide: SideLayoutGuideProvider {} +// The reason why UIView has no extensions of safe area insets and top/bottom guides +// is for iOS10 compat. extension UIView { var sideLayoutGuide: SideLayoutGuideProvider { if #available(iOS 11.0, *) { From 5db27297fbc0c3f15e69c90eb1b5bd76b4f7f2dd Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Wed, 14 Nov 2018 11:11:09 +0900 Subject: [PATCH 066/623] Prevent unexpected assertion failure The assertion always fails on orientation change from landspace to portrait with the default layouts. --- Framework/Sources/FloatingPanelController.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Framework/Sources/FloatingPanelController.swift b/Framework/Sources/FloatingPanelController.swift index c47c3a52..5c9cea0e 100644 --- a/Framework/Sources/FloatingPanelController.swift +++ b/Framework/Sources/FloatingPanelController.swift @@ -208,8 +208,6 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI private func updateLayout(for: UITraitCollection) { floatingPanel.layoutAdapter.layout = fetchLayout(for: view.traitCollection) - assert(floatingPanel.layoutAdapter.layout.supportedPositions.contains(floatingPanel.state)) - guard let parent = parent else { return } floatingPanel.layoutAdapter.prepareLayout(toParent: parent) From 14b792ed81ef6b26d312d2076e9207aa1cf8fb49 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Wed, 14 Nov 2018 10:13:49 +0900 Subject: [PATCH 067/623] Fix scrolling contents shorter than scroll view size --- Framework/Sources/FloatingPanel.swift | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/Framework/Sources/FloatingPanel.swift b/Framework/Sources/FloatingPanel.swift index a261a888..e97c49a0 100644 --- a/Framework/Sources/FloatingPanel.swift +++ b/Framework/Sources/FloatingPanel.swift @@ -20,6 +20,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate didSet { guard let scrollView = scrollView else { return } scrollView.panGestureRecognizer.addTarget(self, action: #selector(handle(panGesture:))) + scrollBouncable = scrollView.bounces scrollIndictorVisible = scrollView.showsVerticalScrollIndicator } } @@ -49,6 +50,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate // Scroll handling private var stopScrollDeceleration: Bool = false + private var scrollBouncable = false private var scrollIndictorVisible = false // MARK: - Interface @@ -206,19 +208,21 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate } // MARK: - Gesture handling - + private let offsetThreshold: CGFloat = 5.0 // Optimal value from testing @objc func handle(panGesture: UIPanGestureRecognizer) { log.debug("Gesture >>>>", panGesture) + let velocity = panGesture.velocity(in: panGesture.view) switch panGesture { case scrollView?.panGestureRecognizer: guard let scrollView = scrollView else { return } - log.debug("SrollPanGesture ScrollView.contentOffset >>>", scrollView.contentOffset.y) + log.debug("SrollPanGesture ScrollView.contentOffset >>>", scrollView.contentOffset.y, scrollView.contentSize, scrollView.bounds.size) - // Prevent scoll slip by the top bounce - if scrollView.isDecelerating == false { - scrollView.bounces = (scrollView.contentOffset.y > 10.0) + // Prevent scoll slip by the top bounce. + // Must the content height less than the scroll view height + if scrollView.isDecelerating == false, scrollView.contentSize.height > scrollView.bounds.height { + scrollView.bounces = (scrollView.contentOffset.y > offsetThreshold) } if surfaceView.frame.minY > layoutAdapter.topY { @@ -249,7 +253,6 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate } case panGesture: let translation = panGesture.translation(in: panGesture.view!.superview) - let velocity = panGesture.velocity(in: panGesture.view) let location = panGesture.location(in: panGesture.view) log.debug(panGesture.state, ">>>", "translation: \(translation.y), velocity: \(velocity.y)") @@ -294,7 +297,8 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate log.debug("ScrollView.contentOffset >>>", scrollView.contentOffset.y) - if scrollView.contentOffset.y - scrollView.contentOffsetZero.y != 0 { + let offset = scrollView.contentOffset.y - scrollView.contentOffsetZero.y + if abs(offset) > offsetThreshold { return true } if scrollView.isDecelerating { @@ -455,7 +459,9 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate stopScrollDeceleration = false // Don't unlock scroll view in animating view when presentation layer != model layer - unlockScrollView() + if targetPosition == .full { + unlockScrollView() + } } private func distance(to targetPosition: FloatingPanelPosition, with translation: CGPoint) -> CGFloat { @@ -664,6 +670,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate else { return } scrollView.isDirectionalLockEnabled = true + scrollView.bounces = false scrollView.showsVerticalScrollIndicator = false } @@ -674,6 +681,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate else { return } scrollView.isDirectionalLockEnabled = false + scrollView.bounces = scrollBouncable scrollView.showsVerticalScrollIndicator = scrollIndictorVisible } From 9edd1c1716648def25d5f8e171da75688cffb159 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Wed, 14 Nov 2018 11:38:54 +0900 Subject: [PATCH 068/623] Add short contents scroll sample --- Examples/Samples/Sources/ViewController.swift | 88 ++++++++++++++++++- 1 file changed, 85 insertions(+), 3 deletions(-) diff --git a/Examples/Samples/Sources/ViewController.swift b/Examples/Samples/Sources/ViewController.swift index 411b16c9..2f973361 100644 --- a/Examples/Samples/Sources/ViewController.swift +++ b/Examples/Samples/Sources/ViewController.swift @@ -14,6 +14,7 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable enum Menu: Int, CaseIterable { case trackingTableView + case trackingShortTableView case trackingTextView case showDetail case showModal @@ -23,8 +24,9 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable var name: String { switch self { - case .trackingTableView: return "Scroll tracking (UITableView)" - case .trackingTextView: return "Scroll tracking (UITextView)" + case .trackingTableView: return "Scroll Tracking TableView" + case .trackingShortTableView: return "Scroll Tracking TableView(short)" + case .trackingTextView: return "Scroll Tracking TextView" case .showDetail: return "Show Detail Panel" case .showModal: return "Show Modal" case .showTabBar: return "Show Tab Bar" @@ -36,6 +38,7 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable var storyboardID: String? { switch self { case .trackingTableView: return nil + case .trackingShortTableView: return nil case .trackingTextView: return "ConsoleViewController" case .showDetail: return "DetailViewController" case .showModal: return "ModalViewController" @@ -80,6 +83,8 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable case let contentVC as DebugTableViewController: mainPanelVC.track(scrollView: contentVC.tableView) + case let contentVC as DebugShortTableViewController: + mainPanelVC.track(scrollView: contentVC.tableView) case let contentVC as NestedScrollViewController: mainPanelVC.track(scrollView: contentVC.scrollView) default: @@ -111,7 +116,12 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let menu = Menu.allCases[indexPath.row] let contentVC: UIViewController = { - guard let storyboardID = menu.storyboardID else { return DebugTableViewController() } + switch menu { + case .trackingTableView: return DebugTableViewController() + case .trackingShortTableView: return DebugShortTableViewController() + default: break + } + guard let storyboardID = menu.storyboardID else { fatalError() } guard let vc = self.storyboard?.instantiateViewController(withIdentifier: storyboardID) else { fatalError() } return vc }() @@ -344,6 +354,78 @@ class DebugTableViewController: UIViewController, UITableViewDataSource, UITable } } +class DebugShortTableViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { + weak var tableView: UITableView! + var items: [String] = [] + override func viewDidLoad() { + super.viewDidLoad() + + let tableView = UITableView(frame: .zero, + style: .plain) + view.addSubview(tableView) + tableView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + tableView.topAnchor.constraint(equalTo: view.topAnchor), + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + tableView.leftAnchor.constraint(equalTo: view.leftAnchor), + tableView.rightAnchor.constraint(equalTo: view.rightAnchor) + ]) + tableView.dataSource = self + tableView.delegate = self + self.tableView = tableView + + let button = UIButton() + button.setTitle("Change item count", for: .normal) + button.setTitleColor(view.tintColor, for: .normal) + button.addTarget(self, action: #selector(changeItemCount), for: .touchUpInside) + view.addSubview(button) + button.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + button.topAnchor.constraint(equalTo: view.topAnchor, constant: 22.0), + button.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -22.0), + ]) + + tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell") + + changeItemCount() + } + + @objc func changeItemCount() { + let max: Int + switch items.count { + case 13: + max = 3 + case 3: + max = 13 + default: + max = 3 + } + items.removeAll() + for i in 0.. Int { + return items.count + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return 45.0 + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) + cell.textLabel?.text = items[indexPath.row] + return cell + } +} + class DetailViewController: UIViewController { @IBOutlet weak var closeButton: UIButton! @IBAction func close(sender: UIButton) { From c3d8422f31871fab4445cd1a8ca132f2a309951e Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Wed, 14 Nov 2018 11:08:39 +0900 Subject: [PATCH 069/623] Fix failure requirements for scroll view's gesture recognizers --- Framework/Sources/FloatingPanel.swift | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/Framework/Sources/FloatingPanel.swift b/Framework/Sources/FloatingPanel.swift index e97c49a0..8bc02587 100644 --- a/Framework/Sources/FloatingPanel.swift +++ b/Framework/Sources/FloatingPanel.swift @@ -173,7 +173,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { guard gestureRecognizer == panGesture else { return false } - log.debug("shouldRecognizeSimultaneouslyWith", otherGestureRecognizer) + /* log.debug("shouldRecognizeSimultaneouslyWith", otherGestureRecognizer) */ return otherGestureRecognizer == scrollView?.panGestureRecognizer } @@ -181,7 +181,10 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { guard gestureRecognizer == panGesture else { return false } - // Do not begin any gestures excluding the tracking scrollView's pan gesture until the pan gesture fails + /* log.debug("shouldBeRequiredToFailBy", otherGestureRecognizer) */ + + // Do not begin any gestures excluding the tracking scrollView's pan gesture + // until the pan gesture fails if otherGestureRecognizer == scrollView?.panGestureRecognizer { return false } else { @@ -192,10 +195,21 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool { guard gestureRecognizer == panGesture else { return false } - // Do not begin the pan gesture until any other gestures fail except fo the tracking scrollView's pan gesture. + /* log.debug("shouldRequireFailureOf", otherGestureRecognizer) */ + + // Do not begin the pan gesture until any other gestures fail except for + // the tracking scrollView's pan gesture and other gestures. + if let scrollView = scrollView { + if scrollView.panGestureRecognizer == otherGestureRecognizer { + return false + } + // For short scroll contents + if scrollView.gestureRecognizers?.contains(otherGestureRecognizer) ?? false { + return false + } + } + switch otherGestureRecognizer { - case scrollView?.panGestureRecognizer: - return false case is UIPanGestureRecognizer, is UISwipeGestureRecognizer, is UIRotationGestureRecognizer, From 5a4b49952e171e5668e059bd274939096f8728a2 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Wed, 14 Nov 2018 20:16:49 +0900 Subject: [PATCH 070/623] Fix comment --- Framework/Sources/FloatingPanel.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Framework/Sources/FloatingPanel.swift b/Framework/Sources/FloatingPanel.swift index 8bc02587..0ae32c77 100644 --- a/Framework/Sources/FloatingPanel.swift +++ b/Framework/Sources/FloatingPanel.swift @@ -233,8 +233,8 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate log.debug("SrollPanGesture ScrollView.contentOffset >>>", scrollView.contentOffset.y, scrollView.contentSize, scrollView.bounds.size) - // Prevent scoll slip by the top bounce. - // Must the content height less than the scroll view height + // Prevent scoll slip by the top bounce when the scroll view's height is + // less than the content's height if scrollView.isDecelerating == false, scrollView.contentSize.height > scrollView.bounds.height { scrollView.bounces = (scrollView.contentOffset.y > offsetThreshold) } From 02c602c9a5f636d90a203904e074a41d59f60da5 Mon Sep 17 00:00:00 2001 From: Ortwin Gentz Date: Wed, 14 Nov 2018 15:30:53 +0100 Subject: [PATCH 071/623] Improved shadow, not darkening content background By removing shadowPath and putting the shadow directly onto the surface view layer instead of a sublayer, the shadow doesn't affect the content view background --- .../Sources/FloatingPanelSurfaceView.swift | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Framework/Sources/FloatingPanelSurfaceView.swift b/Framework/Sources/FloatingPanelSurfaceView.swift index f653e5b6..877b5ce5 100644 --- a/Framework/Sources/FloatingPanelSurfaceView.swift +++ b/Framework/Sources/FloatingPanelSurfaceView.swift @@ -56,7 +56,7 @@ public class FloatingPanelSurfaceView: UIView { /// The color of the surface border. public var borderWidth: CGFloat = 0.0 { didSet { setNeedsLayout() } } - private var shadowLayer: CAShapeLayer! { didSet { setNeedsLayout() } } + private var backgroundLayer: CAShapeLayer! { didSet { setNeedsLayout() } } private struct Default { public static let grabberTopPadding: CGFloat = 6.0 @@ -76,9 +76,9 @@ public class FloatingPanelSurfaceView: UIView { super.backgroundColor = .clear self.clipsToBounds = false - let shadowLayer = CAShapeLayer() - layer.insertSublayer(shadowLayer, at: 0) - self.shadowLayer = shadowLayer + let backgroundLayer = CAShapeLayer() + layer.insertSublayer(backgroundLayer, at: 0) + self.backgroundLayer = backgroundLayer let contentView = FloatingPanelSurfaceContentView() addSubview(contentView) @@ -108,7 +108,7 @@ public class FloatingPanelSurfaceView: UIView { public override func layoutSubviews() { super.layoutSubviews() - updateShadowLayer() + updateLayers() updateContentViewMask() contentView.layer.borderColor = borderColor?.cgColor @@ -116,7 +116,7 @@ public class FloatingPanelSurfaceView: UIView { contentView.backgroundColor = color } - private func updateShadowLayer() { + private func updateLayers() { log.debug("SurfaceView bounds", bounds) var rect = bounds @@ -124,14 +124,14 @@ public class FloatingPanelSurfaceView: UIView { let path = UIBezierPath(roundedRect: rect, byRoundingCorners: [.topLeft, .topRight], cornerRadii: CGSize(width: cornerRadius, height: cornerRadius)) - shadowLayer.path = path.cgPath - shadowLayer.fillColor = color?.cgColor + backgroundLayer.path = path.cgPath + backgroundLayer.fillColor = color?.cgColor + if shadowHidden == false { - shadowLayer.shadowPath = shadowLayer.path - shadowLayer.shadowColor = shadowColor.cgColor - shadowLayer.shadowOffset = shadowOffset - shadowLayer.shadowOpacity = shadowOpacity - shadowLayer.shadowRadius = shadowRadius + layer.shadowColor = shadowColor.cgColor + layer.shadowOffset = shadowOffset + layer.shadowOpacity = shadowOpacity + layer.shadowRadius = shadowRadius } } @@ -156,7 +156,7 @@ public class FloatingPanelSurfaceView: UIView { func set(bottomOverflow: CGFloat) { self.bottomOverflow = bottomOverflow - updateShadowLayer() + updateLayers() updateContentViewMask() } From 1295de3b0a44bddff02fbf1af91268f752d389e3 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Wed, 14 Nov 2018 21:13:10 +0900 Subject: [PATCH 072/623] Improve Scroll tracking(TableView) sample --- Examples/Samples/Sources/ViewController.swift | 179 ++++++++---------- 1 file changed, 82 insertions(+), 97 deletions(-) diff --git a/Examples/Samples/Sources/ViewController.swift b/Examples/Samples/Sources/ViewController.swift index 2f973361..b96261c6 100644 --- a/Examples/Samples/Sources/ViewController.swift +++ b/Examples/Samples/Sources/ViewController.swift @@ -9,12 +9,11 @@ import UIKit import FloatingPanel -class SampleListViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, FloatingPanelControllerDelegate { +class SampleListViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, FloatingPanelControllerDelegate, FloatingPanelLayout { @IBOutlet weak var tableView: UITableView! enum Menu: Int, CaseIterable { case trackingTableView - case trackingShortTableView case trackingTextView case showDetail case showModal @@ -24,9 +23,8 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable var name: String { switch self { - case .trackingTableView: return "Scroll Tracking TableView" - case .trackingShortTableView: return "Scroll Tracking TableView(short)" - case .trackingTextView: return "Scroll Tracking TextView" + case .trackingTableView: return "Scroll tracking(TableView)" + case .trackingTextView: return "Scroll tracking(TextView)" case .showDetail: return "Show Detail Panel" case .showModal: return "Show Modal" case .showTabBar: return "Show Tab Bar" @@ -38,7 +36,6 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable var storyboardID: String? { switch self { case .trackingTableView: return nil - case .trackingShortTableView: return nil case .trackingTextView: return "ConsoleViewController" case .showDetail: return "DetailViewController" case .showModal: return "ModalViewController" @@ -63,6 +60,10 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable addMainPanel(with: contentVC) } + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + } + func addMainPanel(with contentVC: UIViewController) { // Initialize FloatingPanelController mainPanelVC = FloatingPanelController() @@ -83,8 +84,6 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable case let contentVC as DebugTableViewController: mainPanelVC.track(scrollView: contentVC.tableView) - case let contentVC as DebugShortTableViewController: - mainPanelVC.track(scrollView: contentVC.tableView) case let contentVC as NestedScrollViewController: mainPanelVC.track(scrollView: contentVC.scrollView) default: @@ -116,12 +115,7 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let menu = Menu.allCases[indexPath.row] let contentVC: UIViewController = { - switch menu { - case .trackingTableView: return DebugTableViewController() - case .trackingShortTableView: return DebugShortTableViewController() - default: break - } - guard let storyboardID = menu.storyboardID else { fatalError() } + guard let storyboardID = menu.storyboardID else { return DebugTableViewController() } guard let vc = self.storyboard?.instantiateViewController(withIdentifier: storyboardID) else { fatalError() } return vc }() @@ -159,7 +153,19 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable if currentMenu == .showRemovablePanel { return newCollection.verticalSizeClass == .compact ? RemovablePanelLandscapeLayout() : RemovablePanelLayout() } else { - return nil + return self + } + } + + var initialPosition: FloatingPanelPosition { + return .half + } + + func insetFor(position: FloatingPanelPosition) -> CGFloat? { + switch position { + case .full: return UIScreen.main.bounds.height == 667.0 ? 18.0 : 16.0 + case .half: return 262.0 + case .tip: return 69.0 } } } @@ -252,6 +258,7 @@ class DebugTextViewController: UIViewController, UITextViewDelegate { class DebugTableViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { weak var tableView: UITableView! var items: [String] = [] + var itemHeight: CGFloat = 66.0 override func viewDidLoad() { super.viewDidLoad() @@ -269,16 +276,29 @@ class DebugTableViewController: UIViewController, UITableViewDataSource, UITable tableView.delegate = self self.tableView = tableView + let stackView = UIStackView() + view.addSubview(stackView) + stackView.axis = .vertical + stackView.distribution = .fillEqually + stackView.alignment = .trailing + stackView.spacing = 10.0 + stackView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + stackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 22.0), + stackView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -22.0), + ]) + let button = UIButton() button.setTitle("Animate Scroll", for: .normal) button.setTitleColor(view.tintColor, for: .normal) - button.addTarget(self, action: #selector(doScrollAnimate), for: .touchUpInside) - view.addSubview(button) - button.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - button.topAnchor.constraint(equalTo: view.topAnchor, constant: 22.0), - button.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -22.0), - ]) + button.addTarget(self, action: #selector(animateScroll), for: .touchUpInside) + stackView.addArrangedSubview(button) + + let button2 = UIButton() + button2.setTitle("Change content size", for: .normal) + button2.setTitleColor(view.tintColor, for: .normal) + button2.addTarget(self, action: #selector(changeContentSize), for: .touchUpInside) + stackView.addArrangedSubview(button2) for i in 0...100 { items.append("Items \(i)") @@ -286,8 +306,45 @@ class DebugTableViewController: UIViewController, UITableViewDataSource, UITable tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell") } - @objc func doScrollAnimate() { - tableView.scrollToRow(at: IndexPath(row: 50, section: 0), at: .top, animated: true) + @objc func animateScroll() { + tableView.scrollToRow(at: IndexPath(row: lround(Double(items.count) / 2.0), + section: 0), + at: .top, animated: true) + } + + @objc func changeContentSize() { + let actionSheet = UIAlertController(title: "Change content size", message: "", preferredStyle: .actionSheet) + actionSheet.addAction(UIAlertAction(title: "Large", style: .default, handler: { (_) in + self.itemHeight = 66.0 + self.changeItems(100) + })) + actionSheet.addAction(UIAlertAction(title: "Match", style: .default, handler: { (_) in + switch self.tableView.bounds.height { + case 585: // iPhone 6,7,8 + self.itemHeight = self.tableView.bounds.height / 13.0 + self.changeItems(13) + case 656: // iPhone {6,7,8} Plus + self.itemHeight = self.tableView.bounds.height / 16.0 + self.changeItems(16) + default: // iPhone X family + self.itemHeight = self.tableView.bounds.height / 12.0 + self.changeItems(12) + } + })) + actionSheet.addAction(UIAlertAction(title: "Short", style: .default, handler: { (_) in + self.itemHeight = 66.0 + self.changeItems(3) + })) + + self.present(actionSheet, animated: true, completion: nil) + } + + func changeItems(_ count: Int) { + items.removeAll() + for i in 0.. CGFloat { - return 66.0 - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) - cell.textLabel?.text = items[indexPath.row] - return cell - } -} - -class DebugShortTableViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { - weak var tableView: UITableView! - var items: [String] = [] - override func viewDidLoad() { - super.viewDidLoad() - - let tableView = UITableView(frame: .zero, - style: .plain) - view.addSubview(tableView) - tableView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - tableView.topAnchor.constraint(equalTo: view.topAnchor), - tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - tableView.leftAnchor.constraint(equalTo: view.leftAnchor), - tableView.rightAnchor.constraint(equalTo: view.rightAnchor) - ]) - tableView.dataSource = self - tableView.delegate = self - self.tableView = tableView - - let button = UIButton() - button.setTitle("Change item count", for: .normal) - button.setTitleColor(view.tintColor, for: .normal) - button.addTarget(self, action: #selector(changeItemCount), for: .touchUpInside) - view.addSubview(button) - button.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - button.topAnchor.constraint(equalTo: view.topAnchor, constant: 22.0), - button.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -22.0), - ]) - - tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell") - - changeItemCount() - } - - @objc func changeItemCount() { - let max: Int - switch items.count { - case 13: - max = 3 - case 3: - max = 13 - default: - max = 3 - } - items.removeAll() - for i in 0.. Int { - return items.count - } - - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - return 45.0 + return itemHeight } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { From c43afd15d91ccef1314b98f4941618cf0e8e1ca8 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Thu, 15 Nov 2018 08:15:05 +0900 Subject: [PATCH 073/623] Fix unexpected bounciness on short contents This issue happens in dragging from half to full position. --- Framework/Sources/FloatingPanel.swift | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/Framework/Sources/FloatingPanel.swift b/Framework/Sources/FloatingPanel.swift index 0ae32c77..5036ed95 100644 --- a/Framework/Sources/FloatingPanel.swift +++ b/Framework/Sources/FloatingPanel.swift @@ -678,10 +678,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate // MARK: - ScrollView handling private func lockScrollView() { - guard let scrollView = scrollView, - scrollView.isDirectionalLockEnabled == false, - scrollView.showsVerticalScrollIndicator == true - else { return } + guard let scrollView = scrollView else { return } scrollView.isDirectionalLockEnabled = true scrollView.bounces = false @@ -689,10 +686,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate } private func unlockScrollView() { - guard let scrollView = scrollView, - scrollView.isDirectionalLockEnabled == true, - scrollView.showsVerticalScrollIndicator != scrollIndictorVisible - else { return } + guard let scrollView = scrollView else { return } scrollView.isDirectionalLockEnabled = false scrollView.bounces = scrollBouncable From e58f42031e0e4f910db754fbcb806c2b6bc92c0a Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Thu, 15 Nov 2018 08:23:59 +0900 Subject: [PATCH 074/623] Fix a layout bug on orientation changed --- Framework/Sources/FloatingPanelController.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Framework/Sources/FloatingPanelController.swift b/Framework/Sources/FloatingPanelController.swift index 5c9cea0e..dc2b7eb1 100644 --- a/Framework/Sources/FloatingPanelController.swift +++ b/Framework/Sources/FloatingPanelController.swift @@ -205,8 +205,8 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI } } - private func updateLayout(for: UITraitCollection) { - floatingPanel.layoutAdapter.layout = fetchLayout(for: view.traitCollection) + private func updateLayout(for traitCollection: UITraitCollection) { + floatingPanel.layoutAdapter.layout = fetchLayout(for: traitCollection) guard let parent = parent else { return } From 1b528de69576c0cc0380f5e61c1d9b9a83d1e9d6 Mon Sep 17 00:00:00 2001 From: Ortwin Gentz Date: Thu, 15 Nov 2018 22:10:23 +0100 Subject: [PATCH 075/623] Fixed infinite recursion in FloatingPanel.responds(to:) if track(scrollView:) is called twice --- Framework/Sources/FloatingPanelController.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Framework/Sources/FloatingPanelController.swift b/Framework/Sources/FloatingPanelController.swift index dc2b7eb1..76209efd 100644 --- a/Framework/Sources/FloatingPanelController.swift +++ b/Framework/Sources/FloatingPanelController.swift @@ -344,8 +344,10 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI /// public func track(scrollView: UIScrollView) { floatingPanel.scrollView = scrollView - floatingPanel.userScrollViewDelegate = scrollView.delegate - scrollView.delegate = floatingPanel + if scrollView.delegate !== floatingPanel { + floatingPanel.userScrollViewDelegate = scrollView.delegate + scrollView.delegate = floatingPanel + } switch contentInsetAdjustmentBehavior { case .always: if #available(iOS 11.0, *) { From 628b6d3d52dd6f766dce6ada286e9495896744e8 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Fri, 16 Nov 2018 09:31:05 +0900 Subject: [PATCH 076/623] Fix the gesture handling * Fix a detection of a long press gesture in content VC * Fix a SwipeActionPanGesture is not working in the tracking scroll * Update DebugTableViewController to test it --- Examples/Samples/Sources/ViewController.swift | 9 +++ Framework/Sources/FloatingPanel.swift | 63 ++++++++++++++----- 2 files changed, 57 insertions(+), 15 deletions(-) diff --git a/Examples/Samples/Sources/ViewController.swift b/Examples/Samples/Sources/ViewController.swift index b96261c6..b93ea990 100644 --- a/Examples/Samples/Sources/ViewController.swift +++ b/Examples/Samples/Sources/ViewController.swift @@ -409,6 +409,15 @@ class DebugTableViewController: UIViewController, UITableViewDataSource, UITable cell.textLabel?.text = items[indexPath.row] return cell } + + func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? { + return [ + UITableViewRowAction(style: .destructive, title: "Delete", handler: { (action, path) in + self.items.remove(at: path.row) + tableView.deleteRows(at: [path], with: .automatic) + }), + ] + } } class DetailViewController: UIViewController { diff --git a/Framework/Sources/FloatingPanel.swift b/Framework/Sources/FloatingPanel.swift index 5036ed95..56116296 100644 --- a/Framework/Sources/FloatingPanel.swift +++ b/Framework/Sources/FloatingPanel.swift @@ -175,7 +175,9 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate /* log.debug("shouldRecognizeSimultaneouslyWith", otherGestureRecognizer) */ - return otherGestureRecognizer == scrollView?.panGestureRecognizer + // all gestures of the tracking scroll view should be recognized in parallel + // and handle them in self.handle(panGesture:) + return scrollView?.gestureRecognizers?.contains(otherGestureRecognizer) ?? false } public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { @@ -183,28 +185,45 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate /* log.debug("shouldBeRequiredToFailBy", otherGestureRecognizer) */ - // Do not begin any gestures excluding the tracking scrollView's pan gesture - // until the pan gesture fails - if otherGestureRecognizer == scrollView?.panGestureRecognizer { + // The tracking scroll view's gestures should begin without waiting for the pan gesture failure. + // `scrollView.gestureRecognizers` can contains the following gestures + // * UIScrollViewDelayedTouchesBeganGestureRecognizer + // * UIScrollViewPanGestureRecognizer (scrollView.panGestureRecognizer) + // * _UIDragAutoScrollGestureRecognizer + // * _UISwipeActionPanGestureRecognizer + // * UISwipeDismissalGestureRecognizer + if let scrollView = scrollView, + let scrollGestureRecognizers = scrollView.gestureRecognizers, + scrollGestureRecognizers.contains(otherGestureRecognizer) { return false - } else { - return true } + + // Long press gesture should begin without waiting for the pan gesture failure. + if otherGestureRecognizer is UILongPressGestureRecognizer { + return false + } + + // Do not begin any other gestures until the pan gesture fails. + return true } public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool { guard gestureRecognizer == panGesture else { return false } - /* log.debug("shouldRequireFailureOf", otherGestureRecognizer) */ + log.debug("shouldRequireFailureOf", otherGestureRecognizer) - // Do not begin the pan gesture until any other gestures fail except for - // the tracking scrollView's pan gesture and other gestures. + // Should begin the pan gesture without waiting for the tracking scroll view's gestures. + // `scrollView.gestureRecognizers` can contains the following gestures + // * UIScrollViewDelayedTouchesBeganGestureRecognizer + // * UIScrollViewPanGestureRecognizer (scrollView.panGestureRecognizer) + // * _UIDragAutoScrollGestureRecognizer + // * _UISwipeActionPanGestureRecognizer + // * UISwipeDismissalGestureRecognizer if let scrollView = scrollView { - if scrollView.panGestureRecognizer == otherGestureRecognizer { - return false - } - // For short scroll contents - if scrollView.gestureRecognizers?.contains(otherGestureRecognizer) ?? false { + // On short contents scroll, `_UISwipeActionPanGestureRecognizer` blocks + // the panel's pan gesture if not returns false + if let scrollGestureRecognizers = scrollView.gestureRecognizers, + scrollGestureRecognizers.contains(otherGestureRecognizer) { return false } } @@ -215,8 +234,10 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate is UIRotationGestureRecognizer, is UIScreenEdgePanGestureRecognizer, is UIPinchGestureRecognizer: + // Do not begin the pan gesture until these gestures fail return true default: + // Should begin the pan gesture witout waiting tap/long press gestures fail return false } } @@ -299,8 +320,20 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate width: surfaceView.bounds.width, height: FloatingPanelSurfaceView.topGrabberBarHeight * 2) + // When no scrollView, nothing to handle. + guard let scrollView = scrollView else { return false } + + // For _UISwipeActionPanGestureRecognizer + if let scrollGestureRecognizers = scrollView.gestureRecognizers { + for gesture in scrollGestureRecognizers { + if gesture != scrollView.panGestureRecognizer, + gesture.state == .began || gesture.state == .changed { + return true + } + } + } + guard - let scrollView = scrollView, // When no scrollView, nothing to handle. state == .full, // When not .full, don't scroll. interactionInProgress == false, // When interaction already in progress, don't scroll. scrollView.frame.contains(point), // When point not in scrollView, don't scroll. From 40b9aad01119c9dbf2c100a5ef81afdb9eb45031 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Fri, 2 Nov 2018 12:18:26 +0900 Subject: [PATCH 077/623] Release v1.2.0 --- FloatingPanel.podspec | 6 +++--- README.md | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/FloatingPanel.podspec b/FloatingPanel.podspec index dbfa8ff2..b4dc60da 100644 --- a/FloatingPanel.podspec +++ b/FloatingPanel.podspec @@ -1,10 +1,10 @@ Pod::Spec.new do |s| s.name = "FloatingPanel" - s.version = "1.1.0" - s.summary = "FloatingPanel is a simple and easy-to-use UI component of a floating panel interface" + s.version = "1.2.0" + s.summary = "FloatingPanel is a clean and easy-to-use UI component of a floating panel interface." s.description = <<-DESC -FloatingPanel is a simple and easy-to-use UI component for a new interface introduced in Apple Maps, Shortcuts and Stocks app. +FloatingPanel is a clean and easy-to-use UI component for a new interface introduced in Apple Maps, Shortcuts and Stocks app. The new interface displays the related contents and utilities in parallel as a user wants. DESC s.homepage = "https://github.com/SCENEE/FloatingPanel" diff --git a/README.md b/README.md index a6724c13..888013f4 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ The new interface displays the related contents and utilities in parallel as a u - [x] Fluid animation and gesture handling - [x] Scroll view tracking - [x] Common UI elements: Grabber handle, Backdrop and Surface rounding corners -- [x] 2 or 3 anchor positions(full, half, tip) +- [x] 1~3 anchor positions(full, half, tip) - [x] Layout customization for all trait environments(i.e. Landscape orientation support) - [x] Behavior customization - [x] Free from common issues of Auto Layout and gesture handling @@ -99,7 +99,7 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate { // Set a content view controller. let contentVC = ContentViewController() - fpc.show(contentVC, sender: nil) + fpc.set(contentViewController: contentVC) // Track a scroll view(or the siblings) in the content view controller. fpc.track(scrollView: contentVC.tableView) @@ -223,7 +223,7 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate { self.searchPanelVC = FloatingPanelController() let searchVC = SearchViewController() - self.searchPanelVC.show(searchVC, sender: nil) + self.searchPanelVC.set(contentViewController: searchVC) self.searchPanelVC.track(scrollView: contentVC.tableView) self.searchPanelVC.addPanel(toParent: self) @@ -232,7 +232,7 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate { self.detailPanelVC = FloatingPanelController() let contentVC = ContentViewController() - self.detailPanelVC.show(contentVC, sender: nil) + self.detailPanelVC.set(contentViewController: contentVC) self.detailPanelVC.track(scrollView: contentVC.scrollView) self.detailPanelVC.addPanel(toParent: self) From ed8afd8ad7b4be7ae9fb4a58a9831b967c5ce287 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Fri, 16 Nov 2018 09:31:05 +0900 Subject: [PATCH 078/623] Clean up code for scrollGestureRecognizers --- Framework/Sources/FloatingPanel.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Framework/Sources/FloatingPanel.swift b/Framework/Sources/FloatingPanel.swift index 56116296..30df639e 100644 --- a/Framework/Sources/FloatingPanel.swift +++ b/Framework/Sources/FloatingPanel.swift @@ -326,8 +326,10 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate // For _UISwipeActionPanGestureRecognizer if let scrollGestureRecognizers = scrollView.gestureRecognizers { for gesture in scrollGestureRecognizers { - if gesture != scrollView.panGestureRecognizer, - gesture.state == .began || gesture.state == .changed { + guard gesture.state == .began || gesture.state == .changed + else { continue } + + if gesture != scrollView.panGestureRecognizer { return true } } From 9748e04cd6c75c275aa1c5834d47569fecf282b9 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Wed, 21 Nov 2018 09:10:50 +0900 Subject: [PATCH 079/623] Add FloatingPanelControllerDelegate.floatingPanelDidChangePosition(_:) --- Framework/Sources/FloatingPanel.swift | 5 ++++- Framework/Sources/FloatingPanelController.swift | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Framework/Sources/FloatingPanel.swift b/Framework/Sources/FloatingPanel.swift index 30df639e..4b1fefe7 100644 --- a/Framework/Sources/FloatingPanel.swift +++ b/Framework/Sources/FloatingPanel.swift @@ -33,7 +33,10 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate unowned let viewcontroller: FloatingPanelController - private(set) var state: FloatingPanelPosition = .tip + private(set) var state: FloatingPanelPosition = .tip { + didSet { viewcontroller.delegate?.floatingPanelDidChangePosition(viewcontroller) } + } + private var isBottomState: Bool { let remains = layoutAdapter.layout.supportedPositions.filter { $0.rawValue > state.rawValue } return remains.count == 0 diff --git a/Framework/Sources/FloatingPanelController.swift b/Framework/Sources/FloatingPanelController.swift index 76209efd..c7ae696a 100644 --- a/Framework/Sources/FloatingPanelController.swift +++ b/Framework/Sources/FloatingPanelController.swift @@ -12,6 +12,8 @@ public protocol FloatingPanelControllerDelegate: class { // if it returns nil, FloatingPanelController uses the default behavior func floatingPanel(_ vc: FloatingPanelController, behaviorFor newCollection: UITraitCollection) -> FloatingPanelBehavior? + func floatingPanelDidChangePosition(_ vc: FloatingPanelController) // changed the settled position in the model layer + func floatingPanelDidMove(_ vc: FloatingPanelController) // any offset changes // called on start of dragging (may require some time and or distance to move) @@ -34,6 +36,7 @@ public extension FloatingPanelControllerDelegate { func floatingPanel(_ vc: FloatingPanelController, behaviorFor newCollection: UITraitCollection) -> FloatingPanelBehavior? { return nil } + func floatingPanelDidChangePosition(_ vc: FloatingPanelController) {} func floatingPanelDidMove(_ vc: FloatingPanelController) {} func floatingPanelWillBeginDragging(_ vc: FloatingPanelController) {} func floatingPanelDidEndDragging(_ vc: FloatingPanelController, withVelocity velocity: CGPoint, targetPosition: FloatingPanelPosition) {} From e24c83dac076d73413b68c4ffd11b9eff2aca711 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Thu, 22 Nov 2018 14:26:15 +0900 Subject: [PATCH 080/623] Make the animated interaction interruptible --- Framework/Sources/FloatingPanel.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Framework/Sources/FloatingPanel.swift b/Framework/Sources/FloatingPanel.swift index 4b1fefe7..6e752697 100644 --- a/Framework/Sources/FloatingPanel.swift +++ b/Framework/Sources/FloatingPanel.swift @@ -299,6 +299,11 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate return } + if let animator = self.animator { + animator.stopAnimation(true) + self.animator = nil + } + switch panGesture.state { case .began: panningBegan() @@ -480,7 +485,6 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate let targetY = layoutAdapter.positionY(for: targetPosition) let velocityVector = (distance != 0) ? CGVector(dx: 0, dy: max(min(velocity.y/distance, 30.0), -30.0)) : .zero let animator = behavior.interactionAnimator(self.viewcontroller, to: targetPosition, with: velocityVector) - animator.isInterruptible = false // To prevent a backdrop color's punk animator.addAnimations { [weak self] in guard let self = self else { return } if self.state == targetPosition { From 28dc101c8cdc17184d57df41c22482781e649b4b Mon Sep 17 00:00:00 2001 From: Evgeniy Branitsky Date: Thu, 22 Nov 2018 15:55:36 +0300 Subject: [PATCH 081/623] Removing backdrop view --- Framework/Sources/FloatingPanelController.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Framework/Sources/FloatingPanelController.swift b/Framework/Sources/FloatingPanelController.swift index c7ae696a..46f34073 100644 --- a/Framework/Sources/FloatingPanelController.swift +++ b/Framework/Sources/FloatingPanelController.swift @@ -292,6 +292,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI self.willMove(toParent: nil) self.view.removeFromSuperview() + self.backdropView.removeFromSuperview() self.removeFromParent() completion?() } From 334bf83440cf5a9ee0c663bea10e746a1ca4ee86 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Mon, 26 Nov 2018 09:39:14 +0900 Subject: [PATCH 082/623] Release v1.2.1 --- FloatingPanel.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FloatingPanel.podspec b/FloatingPanel.podspec index b4dc60da..f6783fa2 100644 --- a/FloatingPanel.podspec +++ b/FloatingPanel.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "FloatingPanel" - s.version = "1.2.0" + s.version = "1.2.1" s.summary = "FloatingPanel is a clean and easy-to-use UI component of a floating panel interface." s.description = <<-DESC FloatingPanel is a clean and easy-to-use UI component for a new interface introduced in Apple Maps, Shortcuts and Stocks app. From 27e3733f089410c915b0fd6ec4c0a0217662c6b0 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Thu, 29 Nov 2018 10:58:30 +0900 Subject: [PATCH 083/623] Update README --- README.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 888013f4..81ca4f57 100644 --- a/README.md +++ b/README.md @@ -24,14 +24,16 @@ The new interface displays the related contents and utilities in parallel as a u - [Carthage](#carthage) - [Getting Started](#getting-started) - [Usage](#usage) - - [Customize the layout of a floating panel with `FloatingPanelLayout` protocol](#customize-the-layout-of-a-floating-panel-with--floatingpanellayout-protocol) + - [Customize the layout with `FloatingPanelLayout` protocol](#customize-the-layout-with-floatingpanellayout-protocol) - [Change the initial position and height](#change-the-initial-position-and-height) - [Support your landscape layout](#support-your-landscape-layout) - [Customize the behavior with `FloatingPanelBehavior` protocol](#customize-the-behavior-with-floatingpanelbehavior-protocol) - [Modify your floating panel's interaction](#modify-your-floating-panels-interaction) + - [Use a custom grabber handle](#use-a-custom-grabber-handle) + - [Add tap gestures to the surface or backdrop views](#add-tap-gestures-to-the-surface-or-backdrop-views) - [Create an additional floating panel for a detail](#create-an-additional-floating-panel-for-a-detail) - [Move a position with an animation](#move-a-position-with-an-animation) - - [Make your contents correspond with a floating panel behavior](#make-your-contents-correspond-with-a-floating-panel-behavior) + - [Work your contents together with a floating panel behavior](#work-your-contents-together-with-a-floating-panel-behavior) - [Notes](#notes) - ['Show' or 'Show Detail' Segues from `FloatingPanelController`'s content view controller](#show-or-show-detail-segues-from-floatingpanelcontrollers-content-view-controller) - [FloatingPanelSurfaceView's issue on iOS 10](#floatingpanelsurfaceviews-issue-on-ios-10) @@ -119,7 +121,7 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate { ## Usage -### Customize the layout of a floating panel with `FloatingPanelLayout` protocol +### Customize the layout with `FloatingPanelLayout` protocol #### Change the initial position and height @@ -211,6 +213,45 @@ class FloatingPanelStocksBehavior: FloatingPanelBehavior { } ``` +### Use a custom grabber handle + +```swift +class ViewController: UIViewController { + ... + override func viewDidLoad() { + ... + let myGrabberHandleView = MyGrabberHandleView() + fpc.surfaceView.grabberHandle.isHidden = true + fpc.surfaceView.addSubview(myGrabberHandleView) + } + ... +} +``` + +### Add tap gestures to the surface or backdrop views + +```swift +class ViewController: UIViewController, FloatingPanelControllerDelegate { + ... + override func viewDidLoad() { + ... + surfaceTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleSurface(tapGesture:))) + fpc.surfaceView.addGestureRecognizer(surfaceTapGesture) + + backdropTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleBackdrop(tapGesture:))) + fpc.backdropView.addGestureRecognizer(backdropTapGesture) + + surfaceTapGesture.isEnabled = (fpc.position == .tip) + ... + } + ... + // Enable `surfaceTapGesture` only at `tip` position + func floatingPanelDidChangePosition(_ vc: FloatingPanelController) { + surfaceTapGesture.isEnabled = (vc.position == .tip) + } +} +``` + ### Create an additional floating panel for a detail ```swift @@ -257,7 +298,7 @@ In the following example, I move a floating panel to full or half position while } ``` -### Make your contents correspond with a floating panel behavior +### Work your contents together with a floating panel behavior ```swift class ViewController: UIViewController, FloatingPanelControllerDelegate { @@ -311,9 +352,9 @@ class ViewController: UIViewController { A `FloatingPanelController` object proxies an action for `show(_:sender)` to the master VC. That's why the master VC can handle a destination view controller of a 'Show' or 'Show Detail' segue and you can hook `show(_:sender)` to show a secondally floating panel set the destination view controller to the content. -It's a greate way to decouple between a floating panel and the content VC. +It's a great way to decouple between a floating panel and the content VC. -### FloatingPanelSurfaceView's issue on iOS 10 +### FloatingPanelSurfaceView's issue on iOS 10 * On iOS 10, `FloatingPanelSurfaceView.cornerRadius` isn't not automatically masked with the top rounded corners because of UIVisualEffectView issue. See https://forums.developer.apple.com/thread/50854. So you need to draw top rounding corners of your content. Here is an example in Examples/Maps. From 4e54d13253f780dc22ba49e9791f1e6d3c788ea4 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Sat, 1 Dec 2018 11:08:33 +0900 Subject: [PATCH 084/623] Fix an invalid content offset on height change --- Framework/Sources/FloatingPanelController.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Framework/Sources/FloatingPanelController.swift b/Framework/Sources/FloatingPanelController.swift index 46f34073..ce178ab8 100644 --- a/Framework/Sources/FloatingPanelController.swift +++ b/Framework/Sources/FloatingPanelController.swift @@ -198,7 +198,13 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI } private func update(safeAreaInsets: UIEdgeInsets) { + // preserve the current content offset + let contentOffset = scrollView?.contentOffset + floatingPanel.safeAreaInsets = safeAreaInsets + + scrollView?.contentOffset = contentOffset ?? .zero + switch contentInsetAdjustmentBehavior { case .always: scrollView?.contentInset = adjustedContentInsets From 41bc6ca5eab01261971bec85e96581fe384096fa Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Sat, 1 Dec 2018 12:28:26 +0900 Subject: [PATCH 085/623] Fix panning at grabber Area --- Framework/Sources/FloatingPanel.swift | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/Framework/Sources/FloatingPanel.swift b/Framework/Sources/FloatingPanel.swift index 6e752697..f71a9c5b 100644 --- a/Framework/Sources/FloatingPanel.swift +++ b/Framework/Sources/FloatingPanel.swift @@ -245,6 +245,14 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate } } + var grabberAreaFrame: CGRect { + let grabberAreaFrame = CGRect(x: surfaceView.bounds.origin.x, + y: surfaceView.bounds.origin.y, + width: surfaceView.bounds.width, + height: FloatingPanelSurfaceView.topGrabberBarHeight * 2) + return grabberAreaFrame + } + // MARK: - Gesture handling private let offsetThreshold: CGFloat = 5.0 // Optimal value from testing @objc func handle(panGesture: UIPanGestureRecognizer) { @@ -266,8 +274,14 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate if surfaceView.frame.minY > layoutAdapter.topY { switch state { case .full: - // Prevent over scrolling from scroll top in moving the panel from full. - scrollView.contentOffset.y = scrollView.contentOffsetZero.y + let point = panGesture.location(in: surfaceView) + if grabberAreaFrame.contains(point) { + // Preserve the current content offset in moving from full. + scrollView.contentOffset.y = initialScrollOffset.y + } else { + // Prevent over scrolling in moving from full. + scrollView.contentOffset.y = scrollView.contentOffsetZero.y + } case .half, .tip: guard scrollView.isDecelerating == false else { // Don't fix the scroll offset in animating the panel to half and tip. @@ -323,11 +337,6 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate } private func shouldScrollViewHandleTouch(_ scrollView: UIScrollView?, point: CGPoint, velocity: CGPoint) -> Bool { - let grabberBarFrame = CGRect(x: surfaceView.bounds.origin.x, - y: surfaceView.bounds.origin.y, - width: surfaceView.bounds.width, - height: FloatingPanelSurfaceView.topGrabberBarHeight * 2) - // When no scrollView, nothing to handle. guard let scrollView = scrollView else { return false } @@ -347,7 +356,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate state == .full, // When not .full, don't scroll. interactionInProgress == false, // When interaction already in progress, don't scroll. scrollView.frame.contains(point), // When point not in scrollView, don't scroll. - !grabberBarFrame.contains(point) // When point within grabber area, don't scroll. + !grabberAreaFrame.contains(point) // When point within grabber area, don't scroll. else { return false } From f2cd56f48a22ac700852a2abb8f1643047cf0305 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Mon, 3 Dec 2018 09:25:09 +0900 Subject: [PATCH 086/623] Release v1.2.2 --- FloatingPanel.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FloatingPanel.podspec b/FloatingPanel.podspec index f6783fa2..9b956132 100644 --- a/FloatingPanel.podspec +++ b/FloatingPanel.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "FloatingPanel" - s.version = "1.2.1" + s.version = "1.2.2" s.summary = "FloatingPanel is a clean and easy-to-use UI component of a floating panel interface." s.description = <<-DESC FloatingPanel is a clean and easy-to-use UI component for a new interface introduced in Apple Maps, Shortcuts and Stocks app. From 6fa8095cf3bb97491f84d7090bd847e01a9145f4 Mon Sep 17 00:00:00 2001 From: Derek Schade Date: Sat, 17 Nov 2018 18:35:29 +0100 Subject: [PATCH 087/623] add intrinsic viewcontroller to storyboard --- .../Sources/Base.lproj/Main.storyboard | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/Examples/Samples/Sources/Base.lproj/Main.storyboard b/Examples/Samples/Sources/Base.lproj/Main.storyboard index ed108d90..903d6201 100644 --- a/Examples/Samples/Sources/Base.lproj/Main.storyboard +++ b/Examples/Samples/Sources/Base.lproj/Main.storyboard @@ -152,6 +152,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + From 1b51a70ba1b771b6e02a5fca82f320e6c291c06e Mon Sep 17 00:00:00 2001 From: Derek Schade Date: Sat, 17 Nov 2018 18:38:40 +0100 Subject: [PATCH 088/623] Intrinsic layout protocol --- Framework/Sources/FloatingPanelLayout.swift | 23 ++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/Framework/Sources/FloatingPanelLayout.swift b/Framework/Sources/FloatingPanelLayout.swift index b2d9443c..7541e621 100644 --- a/Framework/Sources/FloatingPanelLayout.swift +++ b/Framework/Sources/FloatingPanelLayout.swift @@ -5,6 +5,19 @@ import UIKit +public protocol FloatingPanelIntrinsicLayout: FloatingPanelLayout { + /// Return the viewController that is being displaying the content + var contentViewController: UIViewController? { get set } +} + +public extension FloatingPanelIntrinsicLayout { + var intrinsicHeight: CGFloat { + assert(contentViewController != nil, "Cannot use this if this...") + let fittingSize = UIView.layoutFittingCompressedSize + return contentViewController!.view.systemLayoutSizeFitting(fittingSize).height + } +} + public protocol FloatingPanelLayout: class { /// Returns the initial position of a floating panel. var initialPosition: FloatingPanelPosition { get } @@ -57,6 +70,8 @@ public extension FloatingPanelLayout { } public class FloatingPanelDefaultLayout: FloatingPanelLayout { + public var contentViewController: UIViewController? + public var initialPosition: FloatingPanelPosition { return .half } @@ -71,6 +86,8 @@ public class FloatingPanelDefaultLayout: FloatingPanelLayout { } public class FloatingPanelDefaultLandscapeLayout: FloatingPanelLayout { + public var contentViewController: UIViewController? + public var initialPosition: FloatingPanelPosition { return .tip } @@ -289,11 +306,11 @@ class FloatingPanelLayoutAdapter { } if halfInset > 0 { - assert(halfInset > tipInset, "Invalid half and tip insets") +// assert(halfInset > tipInset, "Invalid half and tip insets") } if fullInset > 0 { - assert(middleY > topY, "Invalid insets") - assert(bottomY > topY, "Invalid insets") +// assert(middleY > topY, "Invalid insets") +// assert(bottomY > topY, "Invalid insets") } } } From a456a0fa7f5a87fa1a0e673ca6630b6fa6ab96c9 Mon Sep 17 00:00:00 2001 From: Derek Schade Date: Sat, 17 Nov 2018 18:38:57 +0100 Subject: [PATCH 089/623] Add IntrinsicPanelLayout --- Examples/Samples/Sources/ViewController.swift | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/Examples/Samples/Sources/ViewController.swift b/Examples/Samples/Sources/ViewController.swift index b93ea990..02db9081 100644 --- a/Examples/Samples/Sources/ViewController.swift +++ b/Examples/Samples/Sources/ViewController.swift @@ -20,6 +20,7 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable case showTabBar case showNestedScrollView case showRemovablePanel + case showIntrinsicView var name: String { switch self { @@ -30,6 +31,7 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable case .showTabBar: return "Show Tab Bar" case .showNestedScrollView: return "Show Nested ScrollView" case .showRemovablePanel: return "Show Removable Panel" + case .showIntrinsicView: return "Show Intrinsic View" } } @@ -42,6 +44,7 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable case .showTabBar: return "TabBarViewController" case .showNestedScrollView: return "NestedScrollViewController" case .showRemovablePanel: return "DetailViewController" + case .showIntrinsicView: return "IntrinsicViewController" } } } @@ -152,6 +155,8 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? { if currentMenu == .showRemovablePanel { return newCollection.verticalSizeClass == .compact ? RemovablePanelLandscapeLayout() : RemovablePanelLayout() + } else if case .showIntrinsicView = currentMenu { + return IntrinsicPanelLayout(mainPanelVC.contentViewController) } else { return self } @@ -170,6 +175,30 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable } } +class IntrinsicPanelLayout: FloatingPanelIntrinsicLayout { + + weak var contentViewController: UIViewController? + + init(_ contentViewController: UIViewController?) { + self.contentViewController = contentViewController + } + + var initialPosition: FloatingPanelPosition { + return .half + } + + var supportedPositions: Set { + return [.half] + } + + func insetFor(position: FloatingPanelPosition) -> CGFloat? { + switch position { + case .half: return intrinsicHeight + default: return nil + } + } +} + class RemovablePanelLayout: FloatingPanelLayout { var initialPosition: FloatingPanelPosition { return .half From ca42a8a8319022867a142a2e286b5476b0c85307 Mon Sep 17 00:00:00 2001 From: Derek Schade Date: Sat, 17 Nov 2018 18:39:12 +0100 Subject: [PATCH 090/623] Update layout when layout is intrinsiclayout --- Framework/Sources/FloatingPanelController.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Framework/Sources/FloatingPanelController.swift b/Framework/Sources/FloatingPanelController.swift index ce178ab8..2b782c42 100644 --- a/Framework/Sources/FloatingPanelController.swift +++ b/Framework/Sources/FloatingPanelController.swift @@ -182,6 +182,9 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI if let parent = parent { self.update(safeAreaInsets: parent.layoutInsets) } + if layout is FloatingPanelIntrinsicLayout { + updateLayout() + } } private func fetchLayout(for traitCollection: UITraitCollection) -> FloatingPanelLayout { From 53b7ce62fc51a10daaffb903aa89bb230a2cb339 Mon Sep 17 00:00:00 2001 From: Derek Schade Date: Sat, 17 Nov 2018 18:41:09 +0100 Subject: [PATCH 091/623] Enabled inset checks again --- Framework/Sources/FloatingPanelLayout.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Framework/Sources/FloatingPanelLayout.swift b/Framework/Sources/FloatingPanelLayout.swift index 7541e621..91464d91 100644 --- a/Framework/Sources/FloatingPanelLayout.swift +++ b/Framework/Sources/FloatingPanelLayout.swift @@ -304,13 +304,13 @@ class FloatingPanelLayoutAdapter { assert(layout.insetFor(position: pos) != nil, "Undefined an inset for a pos(\(pos))") } - + guard !(layout is FloatingPanelIntrinsicLayout) else { return } if halfInset > 0 { -// assert(halfInset > tipInset, "Invalid half and tip insets") + assert(halfInset > tipInset, "Invalid half and tip insets") } if fullInset > 0 { -// assert(middleY > topY, "Invalid insets") -// assert(bottomY > topY, "Invalid insets") + assert(middleY > topY, "Invalid insets") + assert(bottomY > topY, "Invalid insets") } } } From 7782acab6b014ca710ac038b647a0563b161b4b7 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Sat, 24 Nov 2018 10:54:30 +0900 Subject: [PATCH 092/623] Fix unexpected assertion failure --- Framework/Sources/FloatingPanelController.swift | 1 + Framework/Sources/FloatingPanelLayout.swift | 8 ++------ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/Framework/Sources/FloatingPanelController.swift b/Framework/Sources/FloatingPanelController.swift index 2b782c42..dd5a9fef 100644 --- a/Framework/Sources/FloatingPanelController.swift +++ b/Framework/Sources/FloatingPanelController.swift @@ -219,6 +219,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI private func updateLayout(for traitCollection: UITraitCollection) { floatingPanel.layoutAdapter.layout = fetchLayout(for: traitCollection) + floatingPanel.layoutAdapter.checkLayoutConsistance() guard let parent = parent else { return } diff --git a/Framework/Sources/FloatingPanelLayout.swift b/Framework/Sources/FloatingPanelLayout.swift index 91464d91..b0244b41 100644 --- a/Framework/Sources/FloatingPanelLayout.swift +++ b/Framework/Sources/FloatingPanelLayout.swift @@ -110,11 +110,7 @@ class FloatingPanelLayoutAdapter { private weak var surfaceView: FloatingPanelSurfaceView! private weak var backdropView: FloatingPanelBackdropView! - var layout: FloatingPanelLayout { - didSet { - checkLayoutConsistance() - } - } + var layout: FloatingPanelLayout var safeAreaInsets: UIEdgeInsets = .zero { didSet { @@ -304,7 +300,7 @@ class FloatingPanelLayoutAdapter { assert(layout.insetFor(position: pos) != nil, "Undefined an inset for a pos(\(pos))") } - guard !(layout is FloatingPanelIntrinsicLayout) else { return } + if halfInset > 0 { assert(halfInset > tipInset, "Invalid half and tip insets") } From 3674703152f4232060713c9dbee9fae3aaf42459 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Fri, 19 Oct 2018 14:21:37 +0900 Subject: [PATCH 093/623] Swizzling UIViewController.dismiss(animated:completion) for a content VC --- .../Sources/FloatingPanelController.swift | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/Framework/Sources/FloatingPanelController.swift b/Framework/Sources/FloatingPanelController.swift index dd5a9fef..9d3a7800 100644 --- a/Framework/Sources/FloatingPanelController.swift +++ b/Framework/Sources/FloatingPanelController.swift @@ -57,7 +57,6 @@ public enum FloatingPanelPosition: Int, CaseIterable { /// A container view controller to display a floating panel to present contents in parallel as a user wants. /// public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGestureRecognizerDelegate { - /// Constants indicating how safe area insets are added to the adjusted content inset. public enum ContentInsetAdjustmentBehavior: Int { case always @@ -134,6 +133,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI floatingPanel = FloatingPanel(self, layout: fetchLayout(for: self.traitCollection), behavior: fetchBehavior(for: self.traitCollection)) + setUp() } /// Initialize a newly created floating panel controller. @@ -143,6 +143,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI floatingPanel = FloatingPanel(self, layout: fetchLayout(for: self.traitCollection), behavior: fetchBehavior(for: self.traitCollection)) + setUp() } /// Creates the view that the controller manages. @@ -401,3 +402,37 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI } } } + +extension FloatingPanelController { + private static let dismissSwizzling: Any? = { + let aClass: AnyClass! = UIViewController.self //object_getClass(vc) + if let imp = class_getMethodImplementation(aClass, #selector(dismiss(animated:completion:))), + let originalAltMethod = class_getInstanceMethod(aClass, #selector(fp_original_dismiss(animated:completion:))) { + method_setImplementation(originalAltMethod, imp) + } + let originalMethod = class_getInstanceMethod(aClass, #selector(dismiss(animated:completion:))) + let swizzledMethod = class_getInstanceMethod(aClass, #selector(fp_dismiss(animated:completion:))) + if let originalMethod = originalMethod, let swizzledMethod = swizzledMethod { + // switch implementation.. + method_exchangeImplementations(originalMethod, swizzledMethod) + } + return nil + }() + + private func setUp() { + _ = FloatingPanelController.dismissSwizzling + } +} + +public extension UIViewController { + @objc public func fp_original_dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { + // Implementation will be replaced by IMP of self.dismiss(animated:completion:) + } + @objc public func fp_dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { + if let fpc = parent as? FloatingPanelController, fpc.parent != nil { + fpc.removePanelFromParent(animated: flag, completion: completion) + } else { + self.fp_original_dismiss(animated: flag, completion: completion) + } + } +} From 722f752552d4048afaa22f2acf982112593e0e49 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Fri, 19 Oct 2018 14:59:04 +0900 Subject: [PATCH 094/623] Update Samples App to use dismiss(animated:completion) --- Examples/Samples/Sources/ViewController.swift | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Examples/Samples/Sources/ViewController.swift b/Examples/Samples/Sources/ViewController.swift index 02db9081..e8c035a0 100644 --- a/Examples/Samples/Sources/ViewController.swift +++ b/Examples/Samples/Sources/ViewController.swift @@ -278,9 +278,8 @@ class DebugTextViewController: UIViewController, UITextViewDelegate { } @IBAction func close(sender: UIButton) { - // Now impossible - // dismiss(animated: true, completion: nil) - (self.parent as? FloatingPanelController)?.removePanelFromParent(animated: true, completion: nil) + // (self.parent as? FloatingPanelController)?.removePanelFromParent(animated: true, completion: nil) + dismiss(animated: true, completion: nil) } } @@ -452,9 +451,8 @@ class DebugTableViewController: UIViewController, UITableViewDataSource, UITable class DetailViewController: UIViewController { @IBOutlet weak var closeButton: UIButton! @IBAction func close(sender: UIButton) { - // Now impossible - // dismiss(animated: true, completion: nil) - (self.parent as? FloatingPanelController)?.removePanelFromParent(animated: true, completion: nil) + // (self.parent as? FloatingPanelController)?.removePanelFromParent(animated: true, completion: nil) + dismiss(animated: true, completion: nil) } @IBAction func buttonPressed(_ sender: UIButton) { From bbf382b8836ce4f285ee6a5e7a95cd8e15c9441d Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Wed, 21 Nov 2018 11:01:27 +0900 Subject: [PATCH 095/623] FloatingPanelController as a Modality * Change a floating panel view hierarchy * Add FloatingPanelController.{show,hide}(animated:completion) --- .../FloatingPanel.xcodeproj/project.pbxproj | 4 + Framework/Sources/FloatingPanel.swift | 8 +- .../Sources/FloatingPanelController.swift | 121 ++++++++++-------- Framework/Sources/FloatingPanelLayout.swift | 35 +++-- Framework/Sources/FloatingPanelView.swift | 18 +++ 5 files changed, 109 insertions(+), 77 deletions(-) create mode 100644 Framework/Sources/FloatingPanelView.swift diff --git a/Framework/FloatingPanel.xcodeproj/project.pbxproj b/Framework/FloatingPanel.xcodeproj/project.pbxproj index 3fcdfbcf..cab0f3dc 100644 --- a/Framework/FloatingPanel.xcodeproj/project.pbxproj +++ b/Framework/FloatingPanel.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 54352E9821A521CA00CBCA08 /* FloatingPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54352E9721A521CA00CBCA08 /* FloatingPanelView.swift */; }; 5450EEE421646DF500135936 /* FloatingPanelBehavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5450EEE321646DF500135936 /* FloatingPanelBehavior.swift */; }; 545DB9CB2151169500CA77B8 /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 545DB9C12151169500CA77B8 /* FloatingPanel.framework */; }; 545DB9D02151169500CA77B8 /* ViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9CF2151169500CA77B8 /* ViewTests.swift */; }; @@ -32,6 +33,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 54352E9721A521CA00CBCA08 /* FloatingPanelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelView.swift; sourceTree = ""; }; 5450EEE321646DF500135936 /* FloatingPanelBehavior.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelBehavior.swift; sourceTree = ""; }; 545DB9C12151169500CA77B8 /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 545DB9C42151169500CA77B8 /* FloatingPanelController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FloatingPanelController.h; sourceTree = ""; }; @@ -95,6 +97,7 @@ 54CFBFC4215CD09C006B5735 /* FloatingPanel.swift */, 54CFBFC2215CD045006B5735 /* FloatingPanelLayout.swift */, 5450EEE321646DF500135936 /* FloatingPanelBehavior.swift */, + 54352E9721A521CA00CBCA08 /* FloatingPanelView.swift */, 54CDC5D2215B6D5A007D205C /* FloatingPanelSurfaceView.swift */, 54CDC5D4215B6D8D007D205C /* FloatingPanelBackdropView.swift */, 545DBA2A2152383100CA77B8 /* GrabberHandleView.swift */, @@ -225,6 +228,7 @@ 54CDC5D3215B6D5A007D205C /* FloatingPanelSurfaceView.swift in Sources */, 54CFBFC3215CD045006B5735 /* FloatingPanelLayout.swift in Sources */, 54CDC5D5215B6D8D007D205C /* FloatingPanelBackdropView.swift in Sources */, + 54352E9821A521CA00CBCA08 /* FloatingPanelView.swift in Sources */, 54CFBFC5215CD09C006B5735 /* FloatingPanel.swift in Sources */, 54ABD7AF216CCFF7002E6C13 /* Logger.swift in Sources */, 545DB9E021511AC100CA77B8 /* FloatingPanelController.swift in Sources */, diff --git a/Framework/Sources/FloatingPanel.swift b/Framework/Sources/FloatingPanel.swift index f71a9c5b..7c0d4def 100644 --- a/Framework/Sources/FloatingPanel.swift +++ b/Framework/Sources/FloatingPanel.swift @@ -60,7 +60,10 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate init(_ vc: FloatingPanelController, layout: FloatingPanelLayout, behavior: FloatingPanelBehavior) { viewcontroller = vc - surfaceView = vc.view as! FloatingPanelSurfaceView + + surfaceView = FloatingPanelSurfaceView() + surfaceView.backgroundColor = .white + backdropView = FloatingPanelBackdropView() backdropView.backgroundColor = .black backdropView.alpha = 0.0 @@ -88,10 +91,9 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate func setUpViews(in vc: UIViewController) { unowned let view = vc.view! + view.addSubview(surfaceView) view.insertSubview(backdropView, belowSubview: surfaceView) backdropView.frame = view.bounds - - layoutAdapter.prepareLayout(toParent: vc) } func move(to: FloatingPanelPosition, animated: Bool, completion: (() -> Void)? = nil) { diff --git a/Framework/Sources/FloatingPanelController.swift b/Framework/Sources/FloatingPanelController.swift index 9d3a7800..2eb99974 100644 --- a/Framework/Sources/FloatingPanelController.swift +++ b/Framework/Sources/FloatingPanelController.swift @@ -68,7 +68,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI /// Returns the surface view managed by the controller object. It's the same as `self.view`. public var surfaceView: FloatingPanelSurfaceView! { - return view as? FloatingPanelSurfaceView + return floatingPanel.surfaceView } /// Returns the backdrop view managed by the controller object. @@ -101,7 +101,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI return floatingPanel.behavior } - /// The content insets of the tracking scroll view derived from the safe area of the parent view + /// The content insets of the tracking scroll view derived from this safe area public var adjustedContentInsets: UIEdgeInsets { return floatingPanel.layoutAdapter.adjustedContentInsets } @@ -129,33 +129,40 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) - - floatingPanel = FloatingPanel(self, - layout: fetchLayout(for: self.traitCollection), - behavior: fetchBehavior(for: self.traitCollection)) setUp() } /// Initialize a newly created floating panel controller. public init() { super.init(nibName: nil, bundle: nil) + setUp() + } + + private func setUp() { + _ = FloatingPanelController.dismissSwizzling + + modalPresentationStyle = .overCurrentContext floatingPanel = FloatingPanel(self, layout: fetchLayout(for: self.traitCollection), behavior: fetchBehavior(for: self.traitCollection)) - setUp() } /// Creates the view that the controller manages. override public func loadView() { assert(self.storyboard == nil, "Storyboard isn't supported") - let view = FloatingPanelSurfaceView() - view.backgroundColor = .white + let view = FloatingPanelPassThroughView() + view.backgroundColor = .clear self.view = view as UIView } + public override func viewDidLoad() { + super.viewDidLoad() + floatingPanel.setUpViews(in: self) + } + public override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) { super.willTransition(to: newCollection, with: coordinator) @@ -169,20 +176,16 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI super.traitCollectionDidChange(previousTraitCollection) guard previousTraitCollection != traitCollection else { return } - if let parent = parent { - self.update(safeAreaInsets: parent.layoutInsets) - } + self.update(safeAreaInsets: layoutInsets) } 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 parent VC does not call viewSafeAreaInsetsDidChange() expectedly and + // Because the `viewSafeAreaInsetsDidChange()` isn't called expectedly and // `view.safeAreaInsets` has a correct value of the bottom inset here. - if let parent = parent { - self.update(safeAreaInsets: parent.layoutInsets) - } + self.update(safeAreaInsets: layoutInsets) if layout is FloatingPanelIntrinsicLayout { updateLayout() } @@ -221,15 +224,46 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI private func updateLayout(for traitCollection: UITraitCollection) { floatingPanel.layoutAdapter.layout = fetchLayout(for: traitCollection) floatingPanel.layoutAdapter.checkLayoutConsistance() - - guard let parent = parent else { return } - - floatingPanel.layoutAdapter.prepareLayout(toParent: parent) + floatingPanel.layoutAdapter.prepareLayout(in: self) floatingPanel.layoutAdapter.activateLayout(of: floatingPanel.state) } // MARK: - Container view controller interface + 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) + } + + public func hide(animated: Bool = false, completion: (() -> Void)? = nil) { + layoutInsetsObservations.removeAll() + floatingPanel.dismiss(animated: animated, completion: completion) + } + /// Adds the view managed by the controller as a child of the specified view controller. /// - Parameters: /// - parent: A parent view controller object that displays FloatingPanelController's view. A container view controller object isn't applicable. @@ -252,35 +286,17 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI } else { parent.view.addSubview(self.view) } - - layoutInsetsObservations.removeAll() - - // Must track safeAreaInsets/{top,bottom}LayoutGuide of the `parent.view` - // to update floatingPanel.safeAreaInsets`. There are 2 reasons. - // 1. 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 = parent.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() - } + self.view.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + self.view.topAnchor.constraint(equalTo: parent.view.topAnchor, constant: 0.0), + self.view.leftAnchor.constraint(equalTo: parent.view.leftAnchor, constant: 0.0), + self.view.rightAnchor.constraint(equalTo: parent.view.rightAnchor, constant: 0.0), + self.view.bottomAnchor.constraint(equalTo: parent.view.bottomAnchor, constant: 0.0), + ]) parent.addChild(self) - // 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.behavior = fetchBehavior(for: traitCollection) - - floatingPanel.setUpViews(in: parent) - - floatingPanel.present(animated: animated) { [weak self] in + self.show(animated: animated) { [weak self] in guard let self = self else { return } self.didMove(toParent: parent) } @@ -296,14 +312,11 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI return } - layoutInsetsObservations.removeAll() - - floatingPanel.dismiss(animated: animated) { [weak self] in + hide(animated: animated) { [weak self] in guard let self = self else { return } self.willMove(toParent: nil) self.view.removeFromSuperview() - self.backdropView.removeFromSuperview() self.removeFromParent() completion?() } @@ -327,7 +340,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI } if let vc = contentViewController { - let surfaceView = self.view as! FloatingPanelSurfaceView + let surfaceView = floatingPanel.surfaceView surfaceView.add(childView: vc.view) addChild(vc) vc.didMove(toParent: self) @@ -383,7 +396,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI /// by the controller immediately. /// /// This method updates the `FloatingPanelLayout` object from the delegate and - /// then it calls `layoutIfNeeded()` of the parent's root view to force the view + /// then it calls `layoutIfNeeded()` of the root view to force the view /// to update the floating panel's layout immediately. It can be called in an /// animation block. public func updateLayout() { @@ -418,10 +431,6 @@ extension FloatingPanelController { } return nil }() - - private func setUp() { - _ = FloatingPanelController.dismissSwizzling - } } public extension UIViewController { diff --git a/Framework/Sources/FloatingPanelLayout.swift b/Framework/Sources/FloatingPanelLayout.swift index b0244b41..9e978cdf 100644 --- a/Framework/Sources/FloatingPanelLayout.swift +++ b/Framework/Sources/FloatingPanelLayout.swift @@ -106,7 +106,7 @@ public class FloatingPanelDefaultLandscapeLayout: FloatingPanelLayout { class FloatingPanelLayoutAdapter { - private weak var parent: UIViewController! + private weak var vc: UIViewController! private weak var surfaceView: FloatingPanelSurfaceView! private weak var backdropView: FloatingPanelBackdropView! @@ -119,7 +119,6 @@ class FloatingPanelLayoutAdapter { } } - private var parentHeight: CGFloat = 0.0 private var heightBuffer: CGFloat = 88.0 // For bounce private var fixedConstraints: [NSLayoutConstraint] = [] private var fullConstraints: [NSLayoutConstraint] = [] @@ -186,8 +185,8 @@ class FloatingPanelLayoutAdapter { self.backdropView = backdropView } - func prepareLayout(toParent parent: UIViewController) { - self.parent = parent + func prepareLayout(in vc: UIViewController) { + self.vc = vc surfaceView.translatesAutoresizingMaskIntoConstraints = false backdropView.translatesAutoresizingMaskIntoConstraints = false @@ -195,34 +194,34 @@ class FloatingPanelLayoutAdapter { NSLayoutConstraint.deactivate(fixedConstraints + fullConstraints + halfConstraints + tipConstraints + offConstraints) // Fixed constraints of surface and backdrop views - let surfaceConstraints = layout.prepareLayout(surfaceView: surfaceView, in: parent.view!) + let surfaceConstraints = layout.prepareLayout(surfaceView: surfaceView, in: vc.view!) let backdropConstraints = [ - backdropView.topAnchor.constraint(equalTo: parent.view.topAnchor, + backdropView.topAnchor.constraint(equalTo: vc.view.topAnchor, constant: 0.0), - backdropView.leftAnchor.constraint(equalTo: parent.view.leftAnchor, + backdropView.leftAnchor.constraint(equalTo: vc.view.leftAnchor, constant: 0.0), - backdropView.rightAnchor.constraint(equalTo: parent.view.rightAnchor, + backdropView.rightAnchor.constraint(equalTo: vc.view.rightAnchor, constant: 0.0), - backdropView.bottomAnchor.constraint(equalTo: parent.view.bottomAnchor, + backdropView.bottomAnchor.constraint(equalTo: vc.view.bottomAnchor, constant: 0.0), ] fixedConstraints = surfaceConstraints + backdropConstraints // Flexible surface constarints for full, half, tip and off fullConstraints = [ - surfaceView.topAnchor.constraint(equalTo: parent.layoutGuide.topAnchor, + surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.topAnchor, constant: fullInset), ] halfConstraints = [ - surfaceView.topAnchor.constraint(equalTo: parent.layoutGuide.bottomAnchor, + surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.bottomAnchor, constant: -halfInset), ] tipConstraints = [ - surfaceView.topAnchor.constraint(equalTo: parent.layoutGuide.bottomAnchor, + surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.bottomAnchor, constant: -tipInset), ] offConstraints = [ - surfaceView.topAnchor.constraint(equalTo: parent.view.bottomAnchor, constant: 0.0), + surfaceView.topAnchor.constraint(equalTo: vc.view.bottomAnchor, constant: 0.0), ] } @@ -236,11 +235,11 @@ class FloatingPanelLayoutAdapter { } NSLayoutConstraint.deactivate(heightConstraints) - // Must use the parent height, not the screen height because safe area insets - // of the parent are relative values. For example, a view controller in + // Must use the`vc` height, not the screen height because safe area insets + // of `vc` are relative values. For example, a view controller in // Navigation controller's safe area insets and frame can be changed whether // the navigation bar is translucent or not. - let height = self.parent.view.bounds.height - (safeAreaInsets.top + fullInset) + let height = self.vc.view.bounds.height - (safeAreaInsets.top + fullInset) heightConstraints = [ surfaceView.heightAnchor.constraint(equalToConstant: height) ] @@ -305,8 +304,8 @@ class FloatingPanelLayoutAdapter { assert(halfInset > tipInset, "Invalid half and tip insets") } if fullInset > 0 { - assert(middleY > topY, "Invalid insets") - assert(bottomY > topY, "Invalid insets") + assert(middleY > topY, "Invalid insets { topY: \(topY), middleY: \(middleY) }") + assert(bottomY > topY, "Invalid insets { topY: \(topY), bottomY: \(bottomY) }") } } } diff --git a/Framework/Sources/FloatingPanelView.swift b/Framework/Sources/FloatingPanelView.swift new file mode 100644 index 00000000..e29cd9a9 --- /dev/null +++ b/Framework/Sources/FloatingPanelView.swift @@ -0,0 +1,18 @@ +// +// Created by Shin Yamamoto on 2018/11/21. +// Copyright © 2018 Shin Yamamoto. All rights reserved. +// + +import UIKit + +class FloatingPanelPassThroughView: UIView { + public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let view = super.hitTest(point, with: event) + switch view { + case is FloatingPanelPassThroughView: + return nil + default: + return view + } + } +} From 9a60e54cfcb5334c5e94236216b3df3759c34c28 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Wed, 21 Nov 2018 11:12:07 +0900 Subject: [PATCH 096/623] Add 'Show Floating Panel Modal' in Samples app --- Examples/Samples/Sources/ViewController.swift | 51 +++++++++++++++++-- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/Examples/Samples/Sources/ViewController.swift b/Examples/Samples/Sources/ViewController.swift index e8c035a0..8f3ec8b1 100644 --- a/Examples/Samples/Sources/ViewController.swift +++ b/Examples/Samples/Sources/ViewController.swift @@ -17,6 +17,7 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable case trackingTextView case showDetail case showModal + case showFloatingPanelModal case showTabBar case showNestedScrollView case showRemovablePanel @@ -28,6 +29,7 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable case .trackingTextView: return "Scroll tracking(TextView)" case .showDetail: return "Show Detail Panel" case .showModal: return "Show Modal" + case .showFloatingPanelModal: return "Show Floating Panel Modal" case .showTabBar: return "Show Tab Bar" case .showNestedScrollView: return "Show Nested ScrollView" case .showRemovablePanel: return "Show Removable Panel" @@ -41,6 +43,7 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable case .trackingTextView: return "ConsoleViewController" case .showDetail: return "DetailViewController" case .showModal: return "ModalViewController" + case .showFloatingPanelModal: return nil case .showTabBar: return "TabBarViewController" case .showNestedScrollView: return "NestedScrollViewController" case .showRemovablePanel: return "DetailViewController" @@ -144,6 +147,18 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable case .showModal, .showTabBar: let modalVC = contentVC present(modalVC, animated: true, completion: nil) + case .showFloatingPanelModal: + let fpc = FloatingPanelController() + let contentVC = self.storyboard!.instantiateViewController(withIdentifier: "DetailViewController") + fpc.set(contentViewController: contentVC) + fpc.delegate = self + + fpc.surfaceView.cornerRadius = 38.5 + fpc.surfaceView.shadowHidden = false + + fpc.isRemovalInteractionEnabled = true + + self.present(fpc, animated: true, completion: nil) default: detailPanelVC?.removePanelFromParent(animated: true, completion: nil) mainPanelVC?.removePanelFromParent(animated: true) { @@ -153,12 +168,18 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable } func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? { - if currentMenu == .showRemovablePanel { + switch currentMenu { + case .showRemovablePanel: return newCollection.verticalSizeClass == .compact ? RemovablePanelLandscapeLayout() : RemovablePanelLayout() - } else if case .showIntrinsicView = currentMenu { + case .showIntrinsicView: return IntrinsicPanelLayout(mainPanelVC.contentViewController) - } else { - return self + case .showFloatingPanelModal: + if vc != mainPanelVC && vc != detailPanelVC { + return ModalPanelLayout() + } + fallthrough + default: + return (newCollection.verticalSizeClass == .compact) ? nil : self } } @@ -244,6 +265,28 @@ class RemovablePanelLandscapeLayout: FloatingPanelLayout { } } +class ModalPanelLayout: FloatingPanelLayout { + var initialPosition: FloatingPanelPosition { + return .half + } + var supportedPositions: Set { + return [.half] + } + var bottomInteractionBuffer: CGFloat { + return 261.0 - 22.0 + } + + func insetFor(position: FloatingPanelPosition) -> CGFloat? { + switch position { + case .half: return 261.0 + default: return nil + } + } + func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat { + return 0.3 + } +} + class NestedScrollViewController: UIViewController { @IBOutlet weak var scrollView: UIScrollView! From d1ab45d93ac66303c729a58f439f2ad865018169 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Wed, 21 Nov 2018 15:09:54 +0900 Subject: [PATCH 097/623] Add FloatingPanelTransitioning to present as Modality --- .../FloatingPanel.xcodeproj/project.pbxproj | 4 + Framework/Sources/FloatingPanel.swift | 35 ++++--- .../Sources/FloatingPanelController.swift | 4 +- Framework/Sources/FloatingPanelLayout.swift | 3 +- .../Sources/FloatingPanelTransitioning.swift | 92 +++++++++++++++++++ 5 files changed, 124 insertions(+), 14 deletions(-) create mode 100644 Framework/Sources/FloatingPanelTransitioning.swift diff --git a/Framework/FloatingPanel.xcodeproj/project.pbxproj b/Framework/FloatingPanel.xcodeproj/project.pbxproj index cab0f3dc..ace9540d 100644 --- a/Framework/FloatingPanel.xcodeproj/project.pbxproj +++ b/Framework/FloatingPanel.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 54352E9821A521CA00CBCA08 /* FloatingPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54352E9721A521CA00CBCA08 /* FloatingPanelView.swift */; }; + 54352E9621A51A2500CBCA08 /* FloatingPanelTransitioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54352E9521A51A2500CBCA08 /* FloatingPanelTransitioning.swift */; }; 5450EEE421646DF500135936 /* FloatingPanelBehavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5450EEE321646DF500135936 /* FloatingPanelBehavior.swift */; }; 545DB9CB2151169500CA77B8 /* FloatingPanel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 545DB9C12151169500CA77B8 /* FloatingPanel.framework */; }; 545DB9D02151169500CA77B8 /* ViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DB9CF2151169500CA77B8 /* ViewTests.swift */; }; @@ -34,6 +35,7 @@ /* Begin PBXFileReference section */ 54352E9721A521CA00CBCA08 /* FloatingPanelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelView.swift; sourceTree = ""; }; + 54352E9521A51A2500CBCA08 /* FloatingPanelTransitioning.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelTransitioning.swift; sourceTree = ""; }; 5450EEE321646DF500135936 /* FloatingPanelBehavior.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelBehavior.swift; sourceTree = ""; }; 545DB9C12151169500CA77B8 /* FloatingPanel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FloatingPanel.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 545DB9C42151169500CA77B8 /* FloatingPanelController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FloatingPanelController.h; sourceTree = ""; }; @@ -94,6 +96,7 @@ 545DB9C52151169500CA77B8 /* Info.plist */, 545DB9C42151169500CA77B8 /* FloatingPanelController.h */, 545DB9DF21511AC100CA77B8 /* FloatingPanelController.swift */, + 54352E9521A51A2500CBCA08 /* FloatingPanelTransitioning.swift */, 54CFBFC4215CD09C006B5735 /* FloatingPanel.swift */, 54CFBFC2215CD045006B5735 /* FloatingPanelLayout.swift */, 5450EEE321646DF500135936 /* FloatingPanelBehavior.swift */, @@ -234,6 +237,7 @@ 545DB9E021511AC100CA77B8 /* FloatingPanelController.swift in Sources */, 5450EEE421646DF500135936 /* FloatingPanelBehavior.swift in Sources */, 545DBA2B2152383100CA77B8 /* GrabberHandleView.swift in Sources */, + 54352E9621A51A2500CBCA08 /* FloatingPanelTransitioning.swift in Sources */, 545DB9DE215118C800CA77B8 /* UIExtensions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Framework/Sources/FloatingPanel.swift b/Framework/Sources/FloatingPanel.swift index 7c0d4def..23a391bd 100644 --- a/Framework/Sources/FloatingPanel.swift +++ b/Framework/Sources/FloatingPanel.swift @@ -101,7 +101,9 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate } func present(animated: Bool, completion: (() -> Void)? = nil) { - self.layoutAdapter.activateLayout(of: nil) + if animated { + self.layoutAdapter.activateLayout(of: nil) + } move(from: nil, to: layoutAdapter.layout.initialPosition, animated: animated, completion: completion) } @@ -413,7 +415,17 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate endInteraction(for: targetPosition) if isRemovalInteractionEnabled, isBottomState { - if startRemovalAnimation(with: translation, velocity: velocity, distance: distance) { + let velocityVector = (distance != 0) ? CGVector(dx: 0, + dy: max(min(velocity.y/distance, behavior.removalVelocity), 0.0)) : .zero + + if shouldStartRemovalAnimation(with: translation, velocityVector: velocityVector) { + + viewcontroller.delegate?.floatingPanelDidEndDraggingToRemove(viewcontroller, withVelocity: velocity) + self.startRemovalAnimation(with: velocityVector) { [weak self] in + guard let self = self else { return } + self.viewcontroller.dismiss(animated: false) + self.viewcontroller.delegate?.floatingPanelDidEndRemove(self.viewcontroller) + } return } } @@ -424,30 +436,29 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate startAnimation(to: targetPosition, at: distance, with: velocity) } - private func startRemovalAnimation(with translation: CGPoint, velocity: CGPoint, distance: CGFloat) -> Bool { + private func shouldStartRemovalAnimation(with translation: CGPoint, velocityVector: CGVector) -> Bool { let posY = layoutAdapter.positionY(for: state) let currentY = getCurrentY(from: initialFrame, with: translation) let safeAreaBottomY = layoutAdapter.safeAreaBottomY let vth = behavior.removalVelocity let pth = max(min(behavior.removalProgress, 1.0), 0.0) - let velocityVector = (distance != 0) ? CGVector(dx: 0, dy: max(min(velocity.y/distance, vth), 0.0)) : .zero guard (safeAreaBottomY - posY) != 0 else { return false } guard (currentY - posY) / (safeAreaBottomY - posY) >= pth || velocityVector.dy == vth else { return false } - viewcontroller.delegate?.floatingPanelDidEndDraggingToRemove(viewcontroller, withVelocity: velocity) + return true + } + + private func startRemovalAnimation(with velocityVector: CGVector, completion: (() -> Void)?) { let animator = self.behavior.removalInteractionAnimator(self.viewcontroller, with: velocityVector) + animator.addAnimations { [weak self] in - guard let self = self else { return } - self.updateLayout(to: nil) + self?.updateLayout(to: nil) } - animator.addCompletion({ [weak self] (_) in - guard let self = self else { return } - self.viewcontroller.removePanelFromParent(animated: false) - self.viewcontroller.delegate?.floatingPanelDidEndRemove(self.viewcontroller) + animator.addCompletion({ _ in + completion?() }) animator.startAnimation() - return true } private func startInteraction(with translation: CGPoint) { diff --git a/Framework/Sources/FloatingPanelController.swift b/Framework/Sources/FloatingPanelController.swift index 2eb99974..5f8ad451 100644 --- a/Framework/Sources/FloatingPanelController.swift +++ b/Framework/Sources/FloatingPanelController.swift @@ -126,6 +126,7 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI private var floatingPanel: FloatingPanel! private var layoutInsetsObservations: [NSKeyValueObservation] = [] + private let modalTransition = FloatingPanelModalTransition() required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) @@ -141,7 +142,8 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI private func setUp() { _ = FloatingPanelController.dismissSwizzling - modalPresentationStyle = .overCurrentContext + modalPresentationStyle = .custom + transitioningDelegate = modalTransition floatingPanel = FloatingPanel(self, layout: fetchLayout(for: self.traitCollection), diff --git a/Framework/Sources/FloatingPanelLayout.swift b/Framework/Sources/FloatingPanelLayout.swift index 9e978cdf..4a19a898 100644 --- a/Framework/Sources/FloatingPanelLayout.swift +++ b/Framework/Sources/FloatingPanelLayout.swift @@ -228,6 +228,7 @@ class FloatingPanelLayoutAdapter { // The method is separated from prepareLayout(to:) for the rotation support // It must be called in FloatingPanelController.traitCollectionDidChange(_:) func updateHeight() { + guard let vc = vc else { return } defer { UIView.performWithoutAnimation { surfaceView.superview!.layoutIfNeeded() @@ -239,7 +240,7 @@ class FloatingPanelLayoutAdapter { // of `vc` are relative values. For example, a view controller in // Navigation controller's safe area insets and frame can be changed whether // the navigation bar is translucent or not. - let height = self.vc.view.bounds.height - (safeAreaInsets.top + fullInset) + let height = vc.view.bounds.height - (safeAreaInsets.top + fullInset) heightConstraints = [ surfaceView.heightAnchor.constraint(equalToConstant: height) ] diff --git a/Framework/Sources/FloatingPanelTransitioning.swift b/Framework/Sources/FloatingPanelTransitioning.swift new file mode 100644 index 00000000..449de416 --- /dev/null +++ b/Framework/Sources/FloatingPanelTransitioning.swift @@ -0,0 +1,92 @@ +// +// Created by Shin Yamamoto on 2018/11/21. +// Copyright © 2018 Shin Yamamoto. All rights reserved. +// + +import UIKit + +class FloatingPanelModalTransition: NSObject, UIViewControllerTransitioningDelegate { + func animationController(forPresented presented: UIViewController, + presenting: UIViewController, + source: UIViewController) -> UIViewControllerAnimatedTransitioning? { + return FloatingPanelModalPresentTransition() + } + + func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { + return FloatingPanelModalDismissTransition() + } + + func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { + return FloatingPanelPresentationController(presentedViewController: presented, presenting: presenting) + } +} + +class FloatingPanelPresentationController: UIPresentationController { + override func presentationTransitionWillBegin() { + guard + let containerView = self.containerView, + let fpc = presentedViewController as? FloatingPanelController, + let toView = fpc.view + else { fatalError() } + + fpc.view.frame = containerView.bounds + + containerView.addSubview(toView) + toView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + toView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 0.0), + toView.leftAnchor.constraint(equalTo: containerView.leftAnchor, constant: 0.0), + toView.rightAnchor.constraint(equalTo: containerView.rightAnchor, constant: 0.0), + toView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 0.0), + ]) + } + override func presentationTransitionDidEnd(_ completed: Bool) { + if let fpc = presentedViewController as? FloatingPanelController{ + // For non-animated presentation + fpc.show(animated: false) + } + } +} + +class FloatingPanelModalPresentTransition: NSObject, UIViewControllerAnimatedTransitioning { + func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { + guard + let toVC = transitionContext?.viewController(forKey: .to) as? FloatingPanelController + else { fatalError()} + + let animator = toVC.behavior.addAnimator(toVC, to: toVC.layout.initialPosition) + return TimeInterval(animator.duration) + } + + func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { + guard + let toVC = transitionContext.viewController(forKey: .to) as? FloatingPanelController + else { fatalError() } + + toVC.show(animated: true) { + transitionContext.completeTransition(!transitionContext.transitionWasCancelled) + } + } +} + +class FloatingPanelModalDismissTransition: NSObject, UIViewControllerAnimatedTransitioning { + func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { + guard + let fromVC = transitionContext?.viewController(forKey: .from) as? FloatingPanelController + else { fatalError()} + + let animator = fromVC.behavior.removeAnimator(fromVC, from: fromVC.position) + return TimeInterval(animator.duration) + } + + func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { + guard + let fromVC = transitionContext.viewController(forKey: .from) as? FloatingPanelController + else { fatalError() } + + fromVC.hide(animated: true) { + transitionContext.completeTransition(!transitionContext.transitionWasCancelled) + } + } +} + From 34f3c57dab58ba8fa5eca785827d448853603f4a Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Sat, 24 Nov 2018 10:54:10 +0900 Subject: [PATCH 098/623] Fix a backdrop's cut off on orientation change --- Framework/Sources/FloatingPanel.swift | 21 +++++++++++++++++++-- Framework/Sources/FloatingPanelLayout.swift | 12 ++++-------- Framework/Sources/FloatingPanelView.swift | 12 ++++++++++++ 3 files changed, 35 insertions(+), 10 deletions(-) diff --git a/Framework/Sources/FloatingPanel.swift b/Framework/Sources/FloatingPanel.swift index 23a391bd..0286d04a 100644 --- a/Framework/Sources/FloatingPanel.swift +++ b/Framework/Sources/FloatingPanel.swift @@ -91,8 +91,25 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate func setUpViews(in vc: UIViewController) { unowned let view = vc.view! - view.addSubview(surfaceView) - view.insertSubview(backdropView, belowSubview: surfaceView) + // FloatingPanelSurfaceWrapperView is needed to update the surface's height + // without animation and prevent the backdrop's cut-off on orientation change. + let surfaceWrapperView = FloatingPanelSurfaceWrapperView() + surfaceWrapperView.frame = view.bounds + surfaceWrapperView.backgroundColor = .clear + + view.addSubview(surfaceWrapperView) + + surfaceWrapperView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + surfaceWrapperView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0.0), + surfaceWrapperView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0.0), + surfaceWrapperView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0.0), + surfaceWrapperView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0.0), + ]) + + surfaceWrapperView.addSubview(surfaceView) + + view.insertSubview(backdropView, belowSubview: surfaceWrapperView) backdropView.frame = view.bounds } diff --git a/Framework/Sources/FloatingPanelLayout.swift b/Framework/Sources/FloatingPanelLayout.swift index 4a19a898..7a4c460d 100644 --- a/Framework/Sources/FloatingPanelLayout.swift +++ b/Framework/Sources/FloatingPanelLayout.swift @@ -196,14 +196,10 @@ class FloatingPanelLayoutAdapter { // Fixed constraints of surface and backdrop views let surfaceConstraints = layout.prepareLayout(surfaceView: surfaceView, in: vc.view!) let backdropConstraints = [ - backdropView.topAnchor.constraint(equalTo: vc.view.topAnchor, - constant: 0.0), - backdropView.leftAnchor.constraint(equalTo: vc.view.leftAnchor, - constant: 0.0), - backdropView.rightAnchor.constraint(equalTo: vc.view.rightAnchor, - constant: 0.0), - backdropView.bottomAnchor.constraint(equalTo: vc.view.bottomAnchor, - constant: 0.0), + backdropView.topAnchor.constraint(equalTo: vc.view.topAnchor, constant: 0.0), + backdropView.leftAnchor.constraint(equalTo: vc.view.leftAnchor,constant: 0.0), + backdropView.rightAnchor.constraint(equalTo: vc.view.rightAnchor, constant: 0.0), + backdropView.bottomAnchor.constraint(equalTo: vc.view.bottomAnchor, constant: 0.0), ] fixedConstraints = surfaceConstraints + backdropConstraints diff --git a/Framework/Sources/FloatingPanelView.swift b/Framework/Sources/FloatingPanelView.swift index e29cd9a9..a30a91bf 100644 --- a/Framework/Sources/FloatingPanelView.swift +++ b/Framework/Sources/FloatingPanelView.swift @@ -16,3 +16,15 @@ class FloatingPanelPassThroughView: UIView { } } } + +class FloatingPanelSurfaceWrapperView: UIView { + public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let view = super.hitTest(point, with: event) + switch view { + case is FloatingPanelSurfaceWrapperView: + return nil + default: + return view + } + } +} From 51205b9d3b74ca2db86cd0250317f665856a631f Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Mon, 26 Nov 2018 11:07:18 +0900 Subject: [PATCH 099/623] Add .hidden position --- Framework/Sources/FloatingPanel.swift | 45 ++++++++++--------- .../Sources/FloatingPanelController.swift | 3 ++ Framework/Sources/FloatingPanelLayout.swift | 45 +++++++++++-------- 3 files changed, 53 insertions(+), 40 deletions(-) diff --git a/Framework/Sources/FloatingPanel.swift b/Framework/Sources/FloatingPanel.swift index 0286d04a..a3f776ad 100644 --- a/Framework/Sources/FloatingPanel.swift +++ b/Framework/Sources/FloatingPanel.swift @@ -33,7 +33,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate unowned let viewcontroller: FloatingPanelController - private(set) var state: FloatingPanelPosition = .tip { + private(set) var state: FloatingPanelPosition = .hidden { didSet { viewcontroller.delegate?.floatingPanelDidChangePosition(viewcontroller) } } @@ -73,8 +73,6 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate layout: layout) self.behavior = behavior - state = layoutAdapter.layout.initialPosition - panGesture = FloatingPanelPanGestureRecognizer() if #available(iOS 11.0, *) { @@ -119,16 +117,16 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate func present(animated: Bool, completion: (() -> Void)? = nil) { if animated { - self.layoutAdapter.activateLayout(of: nil) + self.layoutAdapter.activateLayout(of: .hidden) } - move(from: nil, to: layoutAdapter.layout.initialPosition, animated: animated, completion: completion) + move(from: .hidden, to: layoutAdapter.layout.initialPosition, animated: animated, completion: completion) } func dismiss(animated: Bool, completion: (() -> Void)? = nil) { - move(from: state, to: nil, animated: animated, completion: completion) + move(from: state, to: .hidden, animated: animated, completion: completion) } - private func move(from: FloatingPanelPosition?, to: FloatingPanelPosition?, animated: Bool, completion: (() -> Void)? = nil) { + private func move(from: FloatingPanelPosition, to: FloatingPanelPosition, animated: Bool, completion: (() -> Void)? = nil) { if to != .full { lockScrollView() } @@ -136,23 +134,19 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate if animated { let animator: UIViewPropertyAnimator switch (from, to) { - case (nil, let to?): + case (.hidden, let to): animator = behavior.addAnimator(self.viewcontroller, to: to) - case (let from?, let to?): - animator = behavior.moveAnimator(self.viewcontroller, from: from, to: to) - case (let from?, nil): + case (let from, .hidden): animator = behavior.removeAnimator(self.viewcontroller, from: from) - case (nil, nil): - fatalError() + case (let from, let to): + animator = behavior.moveAnimator(self.viewcontroller, from: from, to: to) } animator.addAnimations { [weak self] in guard let self = self else { return } self.updateLayout(to: to) - if let to = to { - self.state = to - } + self.state = to } animator.addCompletion { _ in completion?() @@ -160,16 +154,14 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate animator.startAnimation() } else { self.updateLayout(to: to) - if let to = to { - self.state = to - } + self.state = to completion?() } } // MARK: - Layout update - private func updateLayout(to target: FloatingPanelPosition?) { + private func updateLayout(to target: FloatingPanelPosition) { self.layoutAdapter.activateLayout(of: target) } @@ -312,6 +304,8 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate } // Fix the scroll offset in moving the panel from half and tip. scrollView.contentOffset.y = initialScrollOffset.y + case .hidden: + fatalError("A floating panel hidden must not be used by a user") } // Always hide a scroll indicator at the non-top. @@ -470,7 +464,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate let animator = self.behavior.removalInteractionAnimator(self.viewcontroller, with: velocityVector) animator.addAnimations { [weak self] in - self?.updateLayout(to: nil) + self?.updateLayout(to: .hidden) } animator.addCompletion({ _ in completion?() @@ -564,6 +558,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate let middleY = layoutAdapter.middleY let bottomY = layoutAdapter.bottomY let currentY = getCurrentY(from: initialFrame, with: translation) + switch targetPosition { case .full: return CGFloat(fabs(Double(currentY - topY))) @@ -571,6 +566,8 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate return CGFloat(fabs(Double(currentY - middleY))) case .tip: return CGFloat(fabs(Double(currentY - bottomY))) + case .hidden: + fatalError("A floating panel hidden must not be used by a user") } } @@ -603,6 +600,8 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate return .tip } return currentY > middleY ? .half : .full + case .hidden: + fatalError("A floating panel hidden must not be used by a user") } } } @@ -630,6 +629,8 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate return .half case .tip: return currentY > middleY ? .tip : .half + case .hidden: + fatalError("A floating panel hidden must not be used by a user") } } } @@ -682,6 +683,8 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate case .tip: target = .half forwardYDirection = false + case .hidden: + fatalError("A floating panel hidden must not be used by a user") } let redirectionalProgress = max(min(behavior.redirectionalProgress(viewcontroller, from: state, to: target), 1.0), 0.0) diff --git a/Framework/Sources/FloatingPanelController.swift b/Framework/Sources/FloatingPanelController.swift index 5f8ad451..4c92f8f3 100644 --- a/Framework/Sources/FloatingPanelController.swift +++ b/Framework/Sources/FloatingPanelController.swift @@ -51,6 +51,7 @@ public enum FloatingPanelPosition: Int, CaseIterable { case full case half case tip + case hidden } /// @@ -414,6 +415,8 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI return floatingPanel.layoutAdapter.middleY case .tip: return floatingPanel.layoutAdapter.bottomY + case .hidden: + return floatingPanel.layoutAdapter.hiddenY } } } diff --git a/Framework/Sources/FloatingPanelLayout.swift b/Framework/Sources/FloatingPanelLayout.swift index 7a4c460d..2ca4e82c 100644 --- a/Framework/Sources/FloatingPanelLayout.swift +++ b/Framework/Sources/FloatingPanelLayout.swift @@ -81,6 +81,7 @@ public class FloatingPanelDefaultLayout: FloatingPanelLayout { case .full: return 18.0 case .half: return 262.0 case .tip: return 69.0 + case .hidden: return nil } } } @@ -136,6 +137,9 @@ class FloatingPanelLayoutAdapter { private var tipInset: CGFloat { return layout.insetFor(position: .tip) ?? 0.0 } + private var hiddenInset: CGFloat { + return layout.insetFor(position: .hidden) ?? -safeAreaInsets.bottom + } var topY: CGFloat { if layout.supportedPositions.contains(.full) { @@ -157,8 +161,12 @@ class FloatingPanelLayoutAdapter { } } + var hiddenY: CGFloat { + return surfaceView.superview!.bounds.height + } + var safeAreaBottomY: CGFloat { - return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom) + return surfaceView.superview!.bounds.height - (safeAreaInsets.bottom + hiddenInset) } var adjustedContentInsets: UIEdgeInsets { @@ -176,6 +184,8 @@ class FloatingPanelLayoutAdapter { return middleY case .tip: return bottomY + case .hidden: + return hiddenY } } @@ -217,7 +227,8 @@ class FloatingPanelLayoutAdapter { constant: -tipInset), ] offConstraints = [ - surfaceView.topAnchor.constraint(equalTo: vc.view.bottomAnchor, constant: 0.0), + surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.bottomAnchor, + constant: -hiddenInset), ] } @@ -244,21 +255,19 @@ class FloatingPanelLayoutAdapter { surfaceView.set(bottomOverflow: heightBuffer) } - func activateLayout(of state: FloatingPanelPosition?) { + func activateLayout(of state: FloatingPanelPosition) { defer { surfaceView.superview!.layoutIfNeeded() } + + var state = state + setBackdropAlpha(of: state) NSLayoutConstraint.activate(fixedConstraints) - guard var state = state else { - NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + tipConstraints) - NSLayoutConstraint.activate(offConstraints) - return - } - - if layout.supportedPositions.contains(state) == false { + let supportedPositions = layout.supportedPositions.union([.hidden]) + if supportedPositions.contains(state) == false { state = layout.initialPosition } @@ -273,14 +282,17 @@ class FloatingPanelLayoutAdapter { case .tip: NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + offConstraints) NSLayoutConstraint.activate(tipConstraints) + case .hidden: + NSLayoutConstraint.deactivate(fullConstraints + halfConstraints + tipConstraints) + NSLayoutConstraint.activate(offConstraints) } } - func setBackdropAlpha(of target: FloatingPanelPosition?) { - if let target = target { - self.backdropView.alpha = layout.backdropAlphaFor(position: target) - } else { + func setBackdropAlpha(of target: FloatingPanelPosition) { + if target == .hidden { self.backdropView.alpha = 0.0 + } else { + self.backdropView.alpha = layout.backdropAlphaFor(position: target) } } @@ -292,11 +304,6 @@ class FloatingPanelLayoutAdapter { assert(supportedPositions.contains(layout.initialPosition), "Does not include an initial potision(\(layout.initialPosition)) in supportedPositions(\(supportedPositions))") - supportedPositions.forEach { pos in - assert(layout.insetFor(position: pos) != nil, - "Undefined an inset for a pos(\(pos))") - } - if halfInset > 0 { assert(halfInset > tipInset, "Invalid half and tip insets") } From 8ac7e1402214feb786d6811b72be656d1a0ac9f6 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Mon, 26 Nov 2018 11:25:28 +0900 Subject: [PATCH 100/623] Update Samples App for .hidden --- Examples/Samples/Sources/ViewController.swift | 2 ++ Examples/Stocks/Stocks/ViewController.swift | 1 + 2 files changed, 3 insertions(+) diff --git a/Examples/Samples/Sources/ViewController.swift b/Examples/Samples/Sources/ViewController.swift index 8f3ec8b1..8ed8f172 100644 --- a/Examples/Samples/Sources/ViewController.swift +++ b/Examples/Samples/Sources/ViewController.swift @@ -192,6 +192,7 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable case .full: return UIScreen.main.bounds.height == 667.0 ? 18.0 : 16.0 case .half: return 262.0 case .tip: return 69.0 + case .hidden: return nil } } } @@ -591,6 +592,7 @@ class ModalSecondLayout: FloatingPanelLayout { case .full: return 18.0 case .half: return 262.0 case .tip: return 44.0 + case .hidden: return nil } } } diff --git a/Examples/Stocks/Stocks/ViewController.swift b/Examples/Stocks/Stocks/ViewController.swift index bb4ab270..0fc10d55 100644 --- a/Examples/Stocks/Stocks/ViewController.swift +++ b/Examples/Stocks/Stocks/ViewController.swift @@ -114,6 +114,7 @@ class FloatingPanelStocksLayout: FloatingPanelLayout { case .full: return 56.0 case .half: return 262.0 case .tip: return 85.0 + 44.0 // Visible + ToolView + default: return nil } } From e2bbc2cbe916ca58c8911acb829530e366da414f Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Wed, 5 Dec 2018 09:35:43 +0900 Subject: [PATCH 101/623] Let the default interaction animator be uninterruptible Because an interruptible animator causes a wobbling at the animation start. when a user flick a panel quickly to move to full position nearby the position. --- Framework/Sources/FloatingPanel.swift | 3 ++- Framework/Sources/FloatingPanelBehavior.swift | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Framework/Sources/FloatingPanel.swift b/Framework/Sources/FloatingPanel.swift index f71a9c5b..187ed33c 100644 --- a/Framework/Sources/FloatingPanel.swift +++ b/Framework/Sources/FloatingPanel.swift @@ -313,7 +313,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate return } - if let animator = self.animator { + if let animator = self.animator, animator.isInterruptible { animator.stopAnimation(true) self.animator = nil } @@ -491,6 +491,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate } private func startAnimation(to targetPosition: FloatingPanelPosition, at distance: CGFloat, with velocity: CGPoint) { + log.debug("startAnimation", targetPosition, distance, velocity) let targetY = layoutAdapter.positionY(for: targetPosition) let velocityVector = (distance != 0) ? CGVector(dx: 0, dy: max(min(velocity.y/distance, 30.0), -30.0)) : .zero let animator = behavior.interactionAnimator(self.viewcontroller, to: targetPosition, with: velocityVector) diff --git a/Framework/Sources/FloatingPanelBehavior.swift b/Framework/Sources/FloatingPanelBehavior.swift index 0b88062f..e5298219 100644 --- a/Framework/Sources/FloatingPanelBehavior.swift +++ b/Framework/Sources/FloatingPanelBehavior.swift @@ -85,7 +85,9 @@ public extension FloatingPanelBehavior { class FloatingPanelDefaultBehavior: FloatingPanelBehavior { func interactionAnimator(_ fpc: FloatingPanelController, to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator { let timing = timeingCurve(with: velocity) - return UIViewPropertyAnimator(duration: 0, timingParameters: timing) + let animator = UIViewPropertyAnimator(duration: 0, timingParameters: timing) + animator.isInterruptible = false + return animator } private func timeingCurve(with velocity: CGVector) -> UITimingCurveProvider { From 225fbb83039ffa4b67c7e36f3771c1519cb76273 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Wed, 5 Dec 2018 14:08:26 +0900 Subject: [PATCH 102/623] Fix invalid backdrop alpha The bug was found when I commented out `animator.startAnimation()`. --- Framework/Sources/FloatingPanel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Framework/Sources/FloatingPanel.swift b/Framework/Sources/FloatingPanel.swift index 187ed33c..ab85ef56 100644 --- a/Framework/Sources/FloatingPanel.swift +++ b/Framework/Sources/FloatingPanel.swift @@ -568,7 +568,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate } return currentY > middleY ? .tip : .half case .half: - return translation.y >= 0 ? .tip : .full + return currentY > middleY ? .tip : .full case .tip: if translation.y >= 0 { return .tip From bb3693f97c2d557c50c9085c4abc83759be0a22a Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Wed, 28 Nov 2018 17:19:01 +0900 Subject: [PATCH 103/623] 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 a3f776ad..9c56ae28 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 4c92f8f3..a5674746 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 2ca4e82c..a9e5ee46 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 449de416..defb014b 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) } } } From d01e5583559f04db030a0ec4c3c48aa3e0dcccdd Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Thu, 29 Nov 2018 13:13:28 +0900 Subject: [PATCH 104/623] Update comment of FloatingPanelSurfaceView.grabberHandle --- Framework/Sources/FloatingPanelSurfaceView.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Framework/Sources/FloatingPanelSurfaceView.swift b/Framework/Sources/FloatingPanelSurfaceView.swift index 877b5ce5..75320a2c 100644 --- a/Framework/Sources/FloatingPanelSurfaceView.swift +++ b/Framework/Sources/FloatingPanelSurfaceView.swift @@ -10,7 +10,10 @@ class FloatingPanelSurfaceContentView: UIView {} /// A view that presents a surface interface in a floating panel. public class FloatingPanelSurfaceView: UIView { - /// A GrabberHandleView object displayed at the top of the surface view + /// A GrabberHandleView object displayed at the top of the surface view. + /// + /// To use a custom grabber handle, hide this and then add the custom one + /// to the surface view at appropirate coordinates. public var grabberHandle: GrabberHandleView! /// The height of the grabber bar area From 729198de4506ce2c878858f3ce13177d24cd3134 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Thu, 29 Nov 2018 22:06:02 +0900 Subject: [PATCH 105/623] Fix FloatingPanelModalTransitioning --- .../Sources/FloatingPanelTransitioning.swift | 59 +++++++++---------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/Framework/Sources/FloatingPanelTransitioning.swift b/Framework/Sources/FloatingPanelTransitioning.swift index defb014b..0ce07e42 100644 --- a/Framework/Sources/FloatingPanelTransitioning.swift +++ b/Framework/Sources/FloatingPanelTransitioning.swift @@ -22,55 +22,54 @@ class FloatingPanelModalTransition: NSObject, UIViewControllerTransitioningDeleg } class FloatingPanelPresentationController: UIPresentationController { - override func presentationTransitionWillBegin() { - guard - let containerView = self.containerView, - let fpc = presentedViewController as? FloatingPanelController, - let toView = fpc.view - else { fatalError() } - - fpc.view.frame = containerView.bounds - - containerView.addSubview(toView) - toView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - toView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 0.0), - toView.leftAnchor.constraint(equalTo: containerView.leftAnchor, constant: 0.0), - toView.rightAnchor.constraint(equalTo: containerView.rightAnchor, constant: 0.0), - toView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 0.0), - ]) - } override func presentationTransitionDidEnd(_ completed: Bool) { - if let fpc = presentedViewController as? FloatingPanelController{ - // For non-animated presentation + // For non-animated presentation + if let fpc = presentedViewController as? FloatingPanelController, fpc.position == .hidden { fpc.show(animated: false, completion: nil) } } override func dismissalTransitionDidEnd(_ completed: Bool) { - if let fpc = presentedViewController as? FloatingPanelController{ - // For non-animated presentation + // For non-animated dismissal + if let fpc = presentedViewController as? FloatingPanelController, fpc.position != .hidden { fpc.hide(animated: false, completion: nil) } } + + override func containerViewWillLayoutSubviews() { + guard + let containerView = self.containerView, + let fpc = presentedViewController as? FloatingPanelController, + let fpView = fpc.view + else { fatalError() } + + containerView.addSubview(fpView) + fpView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + fpView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 0.0), + fpView.leftAnchor.constraint(equalTo: containerView.leftAnchor, constant: 0.0), + fpView.rightAnchor.constraint(equalTo: containerView.rightAnchor, constant: 0.0), + fpView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 0.0), + ]) + } } class FloatingPanelModalPresentTransition: NSObject, UIViewControllerAnimatedTransitioning { func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { guard - let toVC = transitionContext?.viewController(forKey: .to) as? FloatingPanelController + let fpc = transitionContext?.viewController(forKey: .to) as? FloatingPanelController else { fatalError()} - let animator = toVC.behavior.addAnimator(toVC, to: toVC.layout.initialPosition) + let animator = fpc.behavior.addAnimator(fpc, to: fpc.layout.initialPosition) return TimeInterval(animator.duration) } func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { guard - let toVC = transitionContext.viewController(forKey: .to) as? FloatingPanelController + let fpc = transitionContext.viewController(forKey: .to) as? FloatingPanelController else { fatalError() } - toVC.show(animated: true) { + fpc.show(animated: true) { transitionContext.completeTransition(!transitionContext.transitionWasCancelled) } } @@ -79,19 +78,19 @@ class FloatingPanelModalPresentTransition: NSObject, UIViewControllerAnimatedTra class FloatingPanelModalDismissTransition: NSObject, UIViewControllerAnimatedTransitioning { func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { guard - let fromVC = transitionContext?.viewController(forKey: .from) as? FloatingPanelController + let fpc = transitionContext?.viewController(forKey: .from) as? FloatingPanelController else { fatalError()} - let animator = fromVC.behavior.removeAnimator(fromVC, from: fromVC.position) + let animator = fpc.behavior.removeAnimator(fpc, from: fpc.position) return TimeInterval(animator.duration) } func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { guard - let fromVC = transitionContext.viewController(forKey: .from) as? FloatingPanelController + let fpc = transitionContext.viewController(forKey: .from) as? FloatingPanelController else { fatalError() } - fromVC.hide(animated: true) { + fpc.hide(animated: true) { transitionContext.completeTransition(!transitionContext.transitionWasCancelled) } } From 4ec61ce3611e3b773170c8b6d46ea48213379b1c Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Thu, 29 Nov 2018 22:15:15 +0900 Subject: [PATCH 106/623] Add the modal dismissal on backdrop tap --- Framework/Sources/FloatingPanelTransitioning.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Framework/Sources/FloatingPanelTransitioning.swift b/Framework/Sources/FloatingPanelTransitioning.swift index 0ce07e42..07448513 100644 --- a/Framework/Sources/FloatingPanelTransitioning.swift +++ b/Framework/Sources/FloatingPanelTransitioning.swift @@ -43,6 +43,9 @@ class FloatingPanelPresentationController: UIPresentationController { let fpView = fpc.view else { fatalError() } + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleBackdrop(tapGesture:))) + fpc.backdropView.addGestureRecognizer(tapGesture) + containerView.addSubview(fpView) fpView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ @@ -52,6 +55,10 @@ class FloatingPanelPresentationController: UIPresentationController { fpView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 0.0), ]) } + + @objc func handleBackdrop(tapGesture: UITapGestureRecognizer) { + presentedViewController.dismiss(animated: true, completion: nil) + } } class FloatingPanelModalPresentTransition: NSObject, UIViewControllerAnimatedTransitioning { From b255499978ad1ea8d6de1e686638c4e7684285c8 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Thu, 29 Nov 2018 22:26:41 +0900 Subject: [PATCH 107/623] Fix the condition of removal interaction --- Framework/Sources/FloatingPanel.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Framework/Sources/FloatingPanel.swift b/Framework/Sources/FloatingPanel.swift index 9c56ae28..f6c91379 100644 --- a/Framework/Sources/FloatingPanel.swift +++ b/Framework/Sources/FloatingPanel.swift @@ -443,8 +443,11 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate, UIScrollViewDelegate let vth = behavior.removalVelocity let pth = max(min(behavior.removalProgress, 1.0), 0.0) - guard (safeAreaBottomY - posY) != 0 else { return false } - guard (currentY - posY) / (safeAreaBottomY - posY) >= pth || velocityVector.dy == vth else { return false } + let num = (currentY - posY) + let den = (safeAreaBottomY - posY) + + guard num >= 0, den != 0, (num / den >= pth || velocityVector.dy == vth) + else { return false } return true } From db686f671962ed0e4bfb20f418db1a6b9a45d3dd Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Sat, 1 Dec 2018 10:21:06 +0900 Subject: [PATCH 108/623] Use autoresizing masks instead of Auto Layout constraints --- Framework/Sources/FloatingPanelController.swift | 10 ++-------- Framework/Sources/FloatingPanelTransitioning.swift | 8 +------- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/Framework/Sources/FloatingPanelController.swift b/Framework/Sources/FloatingPanelController.swift index a5674746..f1efc85d 100644 --- a/Framework/Sources/FloatingPanelController.swift +++ b/Framework/Sources/FloatingPanelController.swift @@ -282,19 +282,13 @@ public class FloatingPanelController: UIViewController, UIScrollViewDelegate, UI precondition((parent is UITableViewController) == false, "UITableViewController should not be the parent because the view is a table view so that a floating panel doens't work well") precondition((parent is UICollectionViewController) == false, "UICollectionViewController should not be the parent because the view is a collection view so that a floating panel doens't work well") - view.frame = parent.view.bounds if let belowView = belowView { parent.view.insertSubview(self.view, belowSubview: belowView) } else { parent.view.addSubview(self.view) } - self.view.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - self.view.topAnchor.constraint(equalTo: parent.view.topAnchor, constant: 0.0), - self.view.leftAnchor.constraint(equalTo: parent.view.leftAnchor, constant: 0.0), - self.view.rightAnchor.constraint(equalTo: parent.view.rightAnchor, constant: 0.0), - self.view.bottomAnchor.constraint(equalTo: parent.view.bottomAnchor, constant: 0.0), - ]) + + view.frame = parent.view.bounds // MUST parent.addChild(self) diff --git a/Framework/Sources/FloatingPanelTransitioning.swift b/Framework/Sources/FloatingPanelTransitioning.swift index 07448513..4de74977 100644 --- a/Framework/Sources/FloatingPanelTransitioning.swift +++ b/Framework/Sources/FloatingPanelTransitioning.swift @@ -47,13 +47,7 @@ class FloatingPanelPresentationController: UIPresentationController { fpc.backdropView.addGestureRecognizer(tapGesture) containerView.addSubview(fpView) - fpView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - fpView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 0.0), - fpView.leftAnchor.constraint(equalTo: containerView.leftAnchor, constant: 0.0), - fpView.rightAnchor.constraint(equalTo: containerView.rightAnchor, constant: 0.0), - fpView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 0.0), - ]) + fpView.frame = containerView.bounds //MUST } @objc func handleBackdrop(tapGesture: UITapGestureRecognizer) { From 173425bc68029138db367fdfcd88c3978926060c Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Tue, 4 Dec 2018 22:39:37 +0900 Subject: [PATCH 109/623] Build 'next' branch on Travis CI --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index a02f6e7f..1a46d115 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,7 @@ language: swift branches: only: - master + - next cache: directories: - build From 7c78abd2fa8a9750723e28dffa28d7dcdbe981a4 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Wed, 5 Dec 2018 09:01:54 +0900 Subject: [PATCH 110/623] Fix a bug in Samples app --- Examples/Samples/Sources/ViewController.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/Samples/Sources/ViewController.swift b/Examples/Samples/Sources/ViewController.swift index 8ed8f172..cb470fe7 100644 --- a/Examples/Samples/Sources/ViewController.swift +++ b/Examples/Samples/Sources/ViewController.swift @@ -130,8 +130,8 @@ class SampleListViewController: UIViewController, UITableViewDataSource, UITable switch menu { case .showDetail: - detailPanelVC?.removeFromParent() - + detailPanelVC?.removePanelFromParent(animated: false) + // Initialize FloatingPanelController detailPanelVC = FloatingPanelController() From 1ac580f3cd77e9a768dfb7015063b0dc11ba6c78 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Wed, 5 Dec 2018 16:20:31 +0900 Subject: [PATCH 111/623] Update README - Show/Hide usage - Modality usage --- README.md | 164 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 95 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index 81ca4f57..63733e8f 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,10 @@ The new interface displays the related contents and utilities in parallel as a u - [CocoaPods](#cocoapods) - [Carthage](#carthage) - [Getting Started](#getting-started) + - [Add a floating panel as a child view controller](#add-a-floating-panel-as-a-child-view-controller) + - [Present a floating panel as a modality](#present-a-floating-panel-as-a-modality) - [Usage](#usage) + - [Show/Hide a floating panel in a view with your view hierarchy](#showhide-a-floating-panel-in-a-view-with-your-view-hierarchy) - [Customize the layout with `FloatingPanelLayout` protocol](#customize-the-layout-with-floatingpanellayout-protocol) - [Change the initial position and height](#change-the-initial-position-and-height) - [Support your landscape layout](#support-your-landscape-layout) @@ -52,6 +55,7 @@ The new interface displays the related contents and utilities in parallel as a u - [x] Layout customization for all trait environments(i.e. Landscape orientation support) - [x] Behavior customization - [x] Free from common issues of Auto Layout and gesture handling +- [x] Modal presentation Examples are here. @@ -84,6 +88,8 @@ github "scenee/FloatingPanel" ## Getting Started +### Add a floating panel as a child view controller + ```swift import UIKit import FloatingPanel @@ -112,15 +118,61 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate { override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) + // Remove the views managed by the `FloatingPanelController` object from self.view. fpc.removePanelFromParent() } - ... } ``` +### Present a floating panel as a modality + +```swift +let fpc = FloatingPanelController() +let contentVC = ... +fpc.set(contentViewController: contentVC) + +fpc.isRemovalInteractionEnabled = true // Optional: Let it removable by a swipe-down + +self.present(fpc, animated: true, completion: nil) +``` + +You can show a floating panel over UINavigationController from the containnee view controllers like a Modal of overCurrentContext style. + +NOTE: FloatingPanelController has the custom presentation controller. If you would like to cutomize the presentation/dimissal, please see [FloatingPanelTransitioning](https://github.com/SCENEE/FloatingPanel/blob/feat-modality/Framework/Sources/FloatingPanelTransitioning.swift). + ## Usage +### Show/Hide a floating panel in a view with your view hierarchy + +```swift +// Add the controller and the managed views to a view controller. +// From the second time, just call `show(animated:completion)`. +view.addSubview(fpc.view) +fpc.view.frame = view.bounds // MUST +parent.addChild(fpc) + +// Show a floating panel to the initial position defined in your `FloatingPanelLayout` object. +fpc.show(animated: true) { + + // Only for the first time + self.didMove(toParent: self) +} + +... + +// Hide it +fpc.hide(animated: true) { + + // Remove it if needed + self.willMove(toParent: nil) + self.view.removeFromSuperview() + self.removeFromParent() +} +``` + +NOTE: `FloatingPanelController` wraps `show`/`hide` with `addPanel`/`removePanelFromParent` for easy-to-use. But `show`/`hide` are more convenience for your app. + ### Customize the layout with `FloatingPanelLayout` protocol #### Change the initial position and height @@ -131,7 +183,6 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate { func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? { return MyFloatingPanelLayout() } - ... } class MyFloatingPanelLayout: FloatingPanelLayout { @@ -144,6 +195,7 @@ class MyFloatingPanelLayout: FloatingPanelLayout { case .full: return 16.0 // A top inset from safe area case .half: return 216.0 // A bottom inset from the safe area case .tip: return 44.0 // A bottom inset from the safe area + default: return nil // Or `case .hidden: return nil` } } } @@ -157,7 +209,6 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate { func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? { return (newCollection.verticalSizeClass == .compact) ? FloatingPanelLandscapeLayout() : nil // Returning nil indicates to use the default layout } - ... } class FloatingPanelLandscapeLayout: FloatingPanelLayout { @@ -195,90 +246,67 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate { func floatingPanel(_ vc: FloatingPanelController, behaviorFor newCollection: UITraitCollection) -> FloatingPanelBehavior? { return FloatingPanelStocksBehavior() } - ... } -... class FloatingPanelStocksBehavior: FloatingPanelBehavior { - var velocityThreshold: CGFloat { - return 15.0 - } - + ... func interactionAnimator(_ fpc: FloatingPanelController, to targetPosition: FloatingPanelPosition, with velocity: CGVector) -> UIViewPropertyAnimator { let damping = self.damping(with: velocity) let springTiming = UISpringTimingParameters(dampingRatio: damping, initialVelocity: velocity) return UIViewPropertyAnimator(duration: 0.5, timingParameters: springTiming) } - ... } ``` ### Use a custom grabber handle ```swift -class ViewController: UIViewController { - ... - override func viewDidLoad() { - ... - let myGrabberHandleView = MyGrabberHandleView() - fpc.surfaceView.grabberHandle.isHidden = true - fpc.surfaceView.addSubview(myGrabberHandleView) - } - ... -} +let myGrabberHandleView = MyGrabberHandleView() +fpc.surfaceView.grabberHandle.isHidden = true +fpc.surfaceView.addSubview(myGrabberHandleView) ``` ### Add tap gestures to the surface or backdrop views ```swift -class ViewController: UIViewController, FloatingPanelControllerDelegate { +override func viewDidLoad() { ... - override func viewDidLoad() { - ... - surfaceTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleSurface(tapGesture:))) - fpc.surfaceView.addGestureRecognizer(surfaceTapGesture) + surfaceTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleSurface(tapGesture:))) + fpc.surfaceView.addGestureRecognizer(surfaceTapGesture) - backdropTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleBackdrop(tapGesture:))) - fpc.backdropView.addGestureRecognizer(backdropTapGesture) + backdropTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleBackdrop(tapGesture:))) + fpc.backdropView.addGestureRecognizer(backdropTapGesture) - surfaceTapGesture.isEnabled = (fpc.position == .tip) - ... - } - ... - // Enable `surfaceTapGesture` only at `tip` position - func floatingPanelDidChangePosition(_ vc: FloatingPanelController) { - surfaceTapGesture.isEnabled = (vc.position == .tip) - } + surfaceTapGesture.isEnabled = (fpc.position == .tip) +} + +// Enable `surfaceTapGesture` only at `tip` position +func floatingPanelDidChangePosition(_ vc: FloatingPanelController) { + surfaceTapGesture.isEnabled = (vc.position == .tip) } ``` ### Create an additional floating panel for a detail ```swift -class ViewController: UIViewController, FloatingPanelControllerDelegate { - var searchPanelVC: FloatingPanelController! - var detailPanelVC: FloatingPanelController! - - override func viewDidLoad() { - // Setup Search panel - self.searchPanelVC = FloatingPanelController() +override func viewDidLoad() { + // Setup Search panel + self.searchPanelVC = FloatingPanelController() - let searchVC = SearchViewController() - self.searchPanelVC.set(contentViewController: searchVC) - self.searchPanelVC.track(scrollView: contentVC.tableView) + let searchVC = SearchViewController() + self.searchPanelVC.set(contentViewController: searchVC) + self.searchPanelVC.track(scrollView: contentVC.tableView) - self.searchPanelVC.addPanel(toParent: self) + self.searchPanelVC.addPanel(toParent: self) - // Setup Detail panel - self.detailPanelVC = FloatingPanelController() + // Setup Detail panel + self.detailPanelVC = FloatingPanelController() - let contentVC = ContentViewController() - self.detailPanelVC.set(contentViewController: contentVC) - self.detailPanelVC.track(scrollView: contentVC.scrollView) + let contentVC = ContentViewController() + self.detailPanelVC.set(contentViewController: contentVC) + self.detailPanelVC.track(scrollView: contentVC.scrollView) - self.detailPanelVC.addPanel(toParent: self) - } - ... + self.detailPanelVC.addPanel(toParent: self) } ``` @@ -287,15 +315,15 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate { In the following example, I move a floating panel to full or half position while opening or closing a search bar like Apple Maps. ```swift - func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { - ... - fpc.move(to: .half, animated: true) - } +func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { + ... + fpc.move(to: .half, animated: true) +} - func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) { - ... - fpc.move(to: .full, animated: true) - } +func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) { + ... + fpc.move(to: .full, animated: true) +} ``` ### Work your contents together with a floating panel behavior @@ -315,7 +343,6 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate { searchVC.hideHeader() } } - ... } ``` @@ -323,7 +350,7 @@ class ViewController: UIViewController, FloatingPanelControllerDelegate { ### 'Show' or 'Show Detail' Segues from `FloatingPanelController`'s content view controller -'Show' or 'Show Detail' segues from a content view controller will be managed by a view controller(hereinafter called 'master VC') adding a floating panel. Because a floating panel is just a subview of the master VC. +'Show' or 'Show Detail' segues from a content view controller will be managed by a view controller(hereinafter called 'master VC') adding a floating panel. Because a floating panel is just a subview of the master VC(except for modality). `FloatingPanelController` has no way to manage a stack of view controllers like `UINavigationController`. If so, it would be so complicated and the interface will become `UINavigationController`. This component should not have the responsibility to manage the stack. @@ -346,17 +373,16 @@ class ViewController: UIViewController { secondFpc.addPanel(toParent: self) } - ... } ``` -A `FloatingPanelController` object proxies an action for `show(_:sender)` to the master VC. That's why the master VC can handle a destination view controller of a 'Show' or 'Show Detail' segue and you can hook `show(_:sender)` to show a secondally floating panel set the destination view controller to the content. +A `FloatingPanelController` object proxies an action for `show(_:sender)` to the master VC. That's why the master VC can handle a destination view controller of a 'Show' or 'Show Detail' segue and you can hook `show(_:sender)` to show a secondary floating panel set the destination view controller to the content. It's a great way to decouple between a floating panel and the content VC. ### FloatingPanelSurfaceView's issue on iOS 10 -* On iOS 10, `FloatingPanelSurfaceView.cornerRadius` isn't not automatically masked with the top rounded corners because of UIVisualEffectView issue. See https://forums.developer.apple.com/thread/50854. +* On iOS 10, `FloatingPanelSurfaceView.cornerRadius` isn't not automatically masked with the top rounded corners because of `UIVisualEffectView` issue. See https://forums.developer.apple.com/thread/50854. So you need to draw top rounding corners of your content. Here is an example in Examples/Maps. ```swift override func viewDidLayoutSubviews() { @@ -367,11 +393,11 @@ override func viewDidLayoutSubviews() { } } ``` -* If you sets clear color to `FloatingPanelSurfaceView.backgroundColor`, please note the bottom overflow of your content on bouncing at full position. To prevent it, you need to expand your content. For example, See Example/Maps's Auto Layout settings of UIVisualEffectView in Main.storyborad. +* If you sets clear color to `FloatingPanelSurfaceView.backgroundColor`, please note the bottom overflow of your content on bouncing at full position. To prevent it, you need to expand your content. For example, See Example/Maps App's Auto Layout settings of `UIVisualEffectView` in Main.storyborad. ## Author -Shin Yamamoto +Shin Yamamoto | [@scenee](https://twitter.com/scenee) ## License From 109390f9fff934bd9704f8e2c9e6a9684a7a5ffd Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Sat, 24 Nov 2018 14:44:48 +0900 Subject: [PATCH 112/623] Fix FloatingPanelIntrinsicLayout - `.full` position's height must be the intrinsic height. - Work it for a safe area bottom anchor - Remove FloatingPanelIntrinsicLayout.contentViewController because it isn't actually needed --- .../Sources/Base.lproj/Main.storyboard | 43 ++++----- Examples/Samples/Sources/ViewController.swift | 63 ++----------- Framework/Sources/FloatingPanel.swift | 26 +++++ Framework/Sources/FloatingPanelLayout.swift | 94 +++++++++++++++---- Framework/Sources/UIExtensions.swift | 11 +++ 5 files changed, 143 insertions(+), 94 deletions(-) diff --git a/Examples/Samples/Sources/Base.lproj/Main.storyboard b/Examples/Samples/Sources/Base.lproj/Main.storyboard index 903d6201..aab8863b 100644 --- a/Examples/Samples/Sources/Base.lproj/Main.storyboard +++ b/Examples/Samples/Sources/Base.lproj/Main.storyboard @@ -1,6 +1,6 @@ - + @@ -15,7 +15,7 @@ - + @@ -31,22 +31,22 @@ - + - + - + - + @@ -170,12 +170,6 @@ - + + + + + + + + + + + + + + + + @@ -208,12 +232,6 @@ - + + + + + + + + + + + + @@ -570,9 +587,11 @@ + + @@ -584,6 +603,8 @@ + + @@ -604,7 +625,7 @@ - + diff --git a/Examples/Samples/Sources/ViewController.swift b/Examples/Samples/Sources/ViewController.swift index 15704c63..d94b3581 100644 --- a/Examples/Samples/Sources/ViewController.swift +++ b/Examples/Samples/Sources/ViewController.swift @@ -17,7 +17,7 @@ class SampleListViewController: UIViewController { case trackingTextView case showDetail case showModal - case showFloatingPanelModal + case showPanelModal case showTabBar case showPageView case showNestedScrollView @@ -31,7 +31,7 @@ class SampleListViewController: UIViewController { case .trackingTextView: return "Scroll tracking(TextView)" case .showDetail: return "Show Detail Panel" case .showModal: return "Show Modal" - case .showFloatingPanelModal: return "Show Floating Panel Modal" + case .showPanelModal: return "Show Panel Modal" case .showTabBar: return "Show Tab Bar" case .showPageView: return "Show Page View" case .showNestedScrollView: return "Show Nested ScrollView" @@ -47,7 +47,7 @@ class SampleListViewController: UIViewController { case .trackingTextView: return "ConsoleViewController" case .showDetail: return "DetailViewController" case .showModal: return "ModalViewController" - case .showFloatingPanelModal: return nil + case .showPanelModal: return nil case .showTabBar: return "TabBarViewController" case .showPageView: return nil case .showNestedScrollView: return "NestedScrollViewController" @@ -258,6 +258,7 @@ extension SampleListViewController: UITableViewDelegate { // Initialize FloatingPanelController detailPanelVC = FloatingPanelController() + detailPanelVC.delegate = self // Initialize FloatingPanelController and add the view detailPanelVC.surfaceView.cornerRadius = 6.0 @@ -266,6 +267,9 @@ extension SampleListViewController: UITableViewDelegate { // Set a content view controller detailPanelVC.set(contentViewController: contentVC) + detailPanelVC.contentMode = .fitToBounds + (contentVC as? DetailViewController)?.intrinsicHeightConstraint.priority = .defaultLow + // Add FloatingPanel to self.view detailPanelVC.addPanel(toParent: self, belowView: nil, animated: true) case .showModal, .showTabBar: @@ -287,9 +291,11 @@ extension SampleListViewController: UITableViewDelegate { pageVC.setViewControllers([pages[0]], direction: .forward, animated: false, completion: nil) present(pageVC, animated: true, completion: nil) - case .showFloatingPanelModal: + case .showPanelModal: let fpc = FloatingPanelController() let contentVC = self.storyboard!.instantiateViewController(withIdentifier: "DetailViewController") + contentVC.loadViewIfNeeded() + (contentVC as? DetailViewController)?.modeChangeView.isHidden = true fpc.set(contentViewController: contentVC) fpc.delegate = self @@ -335,7 +341,7 @@ extension SampleListViewController: FloatingPanelControllerDelegate { return newCollection.verticalSizeClass == .compact ? RemovablePanelLandscapeLayout() : RemovablePanelLayout() case .showIntrinsicView: return IntrinsicPanelLayout() - case .showFloatingPanelModal: + case .showPanelModal: if vc != mainPanelVC && vc != detailPanelVC { return ModalPanelLayout() } @@ -723,6 +729,8 @@ extension DebugTableViewController: UITableViewDelegate { } class DetailViewController: InspectableViewController { + @IBOutlet weak var modeChangeView: UIStackView! + @IBOutlet weak var intrinsicHeightConstraint: NSLayoutConstraint! @IBOutlet weak var closeButton: UIButton! @IBAction func close(sender: UIButton) { // (self.parent as? FloatingPanelController)?.removePanelFromParent(animated: true, completion: nil) @@ -739,6 +747,10 @@ class DetailViewController: InspectableViewController { break } } + @IBAction func modeChanged(_ sender: Any) { + guard let fpc = parent as? FloatingPanelController else { return } + fpc.contentMode = (fpc.contentMode == .static) ? .fitToBounds : .static + } @IBAction func tapped(_ sender: Any) { print("Detail panel is tapped!") diff --git a/Framework/Sources/FloatingPanel.swift b/Framework/Sources/FloatingPanel.swift index c0c6a62e..596639e3 100644 --- a/Framework/Sources/FloatingPanel.swift +++ b/Framework/Sources/FloatingPanel.swift @@ -152,7 +152,8 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate { // MARK: - Layout update private func updateLayout(to target: FloatingPanelPosition) { - self.layoutAdapter.activateLayout(of: target) + self.layoutAdapter.activateFixedLayout() + self.layoutAdapter.activateInteractiveLayout(of: target) } func getBackdropAlpha(at currentY: CGFloat, with translation: CGPoint) -> CGFloat { @@ -503,6 +504,8 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate { // from the full position because SafeArea is global in a screen. private func preserveContentVCLayoutIfNeeded() { guard let vc = viewcontroller else { return } + guard vc.contentMode != .fitToBounds else { return } + // Must include topY if (surfaceView.frame.minY <= layoutAdapter.topY) { if !disabledBottomAutoLayout { @@ -709,9 +712,22 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate { let velocityVector = (distance != 0) ? CGVector(dx: 0, dy: abs(velocity.y)/distance) : .zero let animator = behavior.interactionAnimator(vc, to: targetPosition, with: velocityVector) animator.addAnimations { [weak self] in - guard let `self` = self else { return } + guard let `self` = self, let vc = self.viewcontroller else { return } self.state = targetPosition - self.updateLayout(to: targetPosition) + if animator.isInterruptible { + switch vc.contentMode { + case .fitToBounds: + UIView.performWithLinear(startTime: 0.0, relativeDuration: 0.75) { + self.layoutAdapter.activateFixedLayout() + self.surfaceView.superview!.layoutIfNeeded() + } + case .static: + self.layoutAdapter.activateFixedLayout() + } + } else { + self.layoutAdapter.activateFixedLayout() + } + self.layoutAdapter.activateInteractiveLayout(of: targetPosition) } animator.addCompletion { [weak self] pos in // Prevent calling `finishAnimation(at:)` by the old animator whose `isInterruptive` is false diff --git a/Framework/Sources/FloatingPanelController.swift b/Framework/Sources/FloatingPanelController.swift index 70ecb448..e22f0541 100644 --- a/Framework/Sources/FloatingPanelController.swift +++ b/Framework/Sources/FloatingPanelController.swift @@ -114,6 +114,14 @@ open class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGe case never } + /// A flag used to determine how the controller object lays out the content view when the surface position changes. + public enum ContentMode: Int { + /// The option to fix the content to keep the height of the top most position. + case `static` + /// The option to scale the content to fit the bounds of the root view by changing the surface position. + case fitToBounds + } + /// The delegate of the floating panel controller object. public weak var delegate: FloatingPanelControllerDelegate?{ didSet{ @@ -177,6 +185,14 @@ open class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGe set { set(contentViewController: newValue) } get { return _contentViewController } } + + public var contentMode: ContentMode = .static { + didSet { + guard position != .hidden else { return } + activateLayout() + } + } + private var _contentViewController: UIViewController? private(set) var floatingPanel: FloatingPanel! @@ -308,7 +324,6 @@ open class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGe private func reloadLayout(for traitCollection: UITraitCollection) { floatingPanel.layoutAdapter.layout = fetchLayout(for: traitCollection) - floatingPanel.layoutAdapter.prepareLayout(in: self) if let parent = self.parent { if let layout = layout as? UIViewController, layout == parent { @@ -321,6 +336,8 @@ open class FloatingPanelController: UIViewController, UIScrollViewDelegate, UIGe } private func activateLayout() { + floatingPanel.layoutAdapter.prepareLayout(in: self) + // preserve the current content offset let contentOffset = scrollView?.contentOffset diff --git a/Framework/Sources/FloatingPanelLayout.swift b/Framework/Sources/FloatingPanelLayout.swift index 61020753..08a6501b 100644 --- a/Framework/Sources/FloatingPanelLayout.swift +++ b/Framework/Sources/FloatingPanelLayout.swift @@ -184,6 +184,8 @@ class FloatingPanelLayoutAdapter { private var tipConstraints: [NSLayoutConstraint] = [] private var offConstraints: [NSLayoutConstraint] = [] private var interactiveTopConstraint: NSLayoutConstraint? + private var bottomConstraint: NSLayoutConstraint? + private var heightConstraint: NSLayoutConstraint? @@ -304,6 +306,10 @@ class FloatingPanelLayoutAdapter { self.vc = vc NSLayoutConstraint.deactivate(fixedConstraints + fullConstraints + halfConstraints + tipConstraints + offConstraints) + NSLayoutConstraint.deactivate(constraint: self.heightConstraint) + self.heightConstraint = nil + NSLayoutConstraint.deactivate(constraint: self.bottomConstraint) + self.bottomConstraint = nil surfaceView.translatesAutoresizingMaskIntoConstraints = false backdropView.translatesAutoresizingMaskIntoConstraints = false @@ -319,6 +325,11 @@ class FloatingPanelLayoutAdapter { fixedConstraints = surfaceConstraints + backdropConstraints + if vc.contentMode == .fitToBounds { + bottomConstraint = surfaceView.bottomAnchor.constraint(equalTo: vc.view.bottomAnchor, + constant: 0.0) + } + // Flexible surface constraints for full, half, tip and off let topAnchor: NSLayoutYAxisAnchor = { if layout.positionReference == .fromSuperview { @@ -384,41 +395,48 @@ class FloatingPanelLayoutAdapter { func endInteraction(at state: FloatingPanelPosition) { // Don't deactivate `interactiveTopConstraint` here because it leads to // unsatisfiable constraints + + if self.interactiveTopConstraint == nil { + // Actiavate `interactiveTopConstraint` for `fitToBounds` mode. + // It goes throught this path when the pan gesture state jumps + // from .begin to .end. + startInteraction(at: state) + } } // The method is separated from prepareLayout(to:) for the rotation support // It must be called in FloatingPanelController.traitCollectionDidChange(_:) func updateHeight() { guard let vc = vc else { return } + NSLayoutConstraint.deactivate(constraint: heightConstraint) + heightConstraint = nil - if let const = self.heightConstraint { - NSLayoutConstraint.deactivate([const]) + if layout is FloatingPanelIntrinsicLayout { + updateIntrinsicHeight() + } + defer { + if layout is FloatingPanelIntrinsicLayout { + NSLayoutConstraint.deactivate(fullConstraints) + fullConstraints = [ + surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.bottomAnchor, + constant: -fullInset), + ] + } } - let heightConstraint: NSLayoutConstraint + guard vc.contentMode != .fitToBounds else { return } switch layout { case is FloatingPanelIntrinsicLayout: - updateIntrinsicHeight() heightConstraint = surfaceView.heightAnchor.constraint(equalToConstant: intrinsicHeight + safeAreaInsets.bottom) default: let const = -(positionY(for: topMostState)) heightConstraint = surfaceView.heightAnchor.constraint(equalTo: vc.view.heightAnchor, constant: const) } - - NSLayoutConstraint.activate([heightConstraint]) - self.heightConstraint = heightConstraint + NSLayoutConstraint.activate(constraint: heightConstraint) surfaceView.bottomOverflow = vc.view.bounds.height + layout.topInteractionBuffer - - if layout is FloatingPanelIntrinsicLayout { - NSLayoutConstraint.deactivate(fullConstraints) - fullConstraints = [ - surfaceView.topAnchor.constraint(equalTo: vc.layoutGuide.bottomAnchor, - constant: -fullInset), - ] - } } func updateInteractiveTopConstraint(diff: CGFloat, allowsTopBuffer: Bool, with behavior: FloatingPanelBehavior) { @@ -475,7 +493,19 @@ class FloatingPanelLayoutAdapter { return (1.0 - (1.0 / ((buffer * 0.55 / base) + 1.0))) * base } - func activateLayout(of state: FloatingPanelPosition) { + func activateFixedLayout() { + // Must deactivate `interactiveTopConstraint` here + NSLayoutConstraint.deactivate(constraint: self.interactiveTopConstraint) + self.interactiveTopConstraint = nil + + NSLayoutConstraint.activate(fixedConstraints) + + if vc.contentMode == .fitToBounds { + NSLayoutConstraint.activate(constraint: self.bottomConstraint) + } + } + + func activateInteractiveLayout(of state: FloatingPanelPosition) { defer { surfaceView.superview!.layoutIfNeeded() } @@ -484,13 +514,6 @@ class FloatingPanelLayoutAdapter { setBackdropAlpha(of: state) - // Must deactivate `interactiveTopConstraint` here - if let interactiveTopConstraint = interactiveTopConstraint { - NSLayoutConstraint.deactivate([interactiveTopConstraint]) - self.interactiveTopConstraint = nil - } - NSLayoutConstraint.activate(fixedConstraints) - if isValid(state) == false { state = layout.initialPosition } @@ -508,6 +531,11 @@ class FloatingPanelLayoutAdapter { } } + func activateLayout(of state: FloatingPanelPosition) { + activateFixedLayout() + activateInteractiveLayout(of: state) + } + func isValid(_ state: FloatingPanelPosition) -> Bool { return supportedPositions.union([.hidden]).contains(state) } diff --git a/Framework/Sources/UIExtensions.swift b/Framework/Sources/UIExtensions.swift index 78dd3b12..7ce4d2fd 100644 --- a/Framework/Sources/UIExtensions.swift +++ b/Framework/Sources/UIExtensions.swift @@ -75,6 +75,12 @@ extension UIView { func enableAutoLayout() { translatesAutoresizingMaskIntoConstraints = false } + + static func performWithLinear(startTime: Double = 0.0, relativeDuration: Double = 1.0, _ animations: @escaping (() -> Void)) { + UIView.animateKeyframes(withDuration: 0.0, delay: 0.0, options: [.calculationModeCubic], animations: { + UIView.addKeyframe(withRelativeStartTime: startTime, relativeDuration: relativeDuration, animations: animations) + }, completion: nil) + } } #if __FP_LOG @@ -140,3 +146,14 @@ extension UITraitCollection { || previous.layoutDirection != layoutDirection } } + +extension NSLayoutConstraint { + static func activate(constraint: NSLayoutConstraint?) { + guard let constraint = constraint else { return } + self.activate([constraint]) + } + static func deactivate(constraint: NSLayoutConstraint?) { + guard let constraint = constraint else { return } + self.deactivate([constraint]) + } +} diff --git a/Framework/Tests/FloatingPanelControllerTests.swift b/Framework/Tests/FloatingPanelControllerTests.swift index 3c60ca18..fade4299 100644 --- a/Framework/Tests/FloatingPanelControllerTests.swift +++ b/Framework/Tests/FloatingPanelControllerTests.swift @@ -107,6 +107,31 @@ class FloatingPanelControllerTests: XCTestCase { fpc.move(to: .hidden, animated: false) XCTAssertEqual(fpc.surfaceView.frame.minY, fpc.originYOfSurface(for: .hidden)) } + + func test_contentMode() { + let fpc = FloatingPanelController(delegate: nil) + fpc.loadViewIfNeeded() + fpc.view.frame = CGRect(x: 0, y: 0, width: 375, height: 667) + fpc.show(animated: false, completion: nil) + + fpc.contentMode = .static + + fpc.move(to: .full, animated: false) + XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.originYOfSurface(for: .full)) + fpc.move(to: .half, animated: false) + XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.originYOfSurface(for: .full)) + fpc.move(to: .tip, animated: false) + XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.originYOfSurface(for: .full)) + + fpc.contentMode = .fitToBounds + + fpc.move(to: .full, animated: false) + XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.originYOfSurface(for: .full)) + fpc.move(to: .half, animated: false) + XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.originYOfSurface(for: .half)) + fpc.move(to: .tip, animated: false) + XCTAssertEqual(fpc.surfaceView.frame.height, fpc.view.bounds.height - fpc.originYOfSurface(for: .tip)) + } } private class MyZombieViewController: UIViewController, FloatingPanelLayout, FloatingPanelBehavior, FloatingPanelControllerDelegate { diff --git a/README.md b/README.md index 60c5c1bb..bfa940ba 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ The new interface displays the related contents and utilities in parallel as a u - [View hierarchy](#view-hierarchy) - [Usage](#usage) - [Show/Hide a floating panel in a view with your view hierarchy](#showhide-a-floating-panel-in-a-view-with-your-view-hierarchy) + - [Scale the content view when the surface position changes](#scale-the-content-view-when-the-surface-position-changes) - [Customize the layout with `FloatingPanelLayout` protocol](#customize-the-layout-with-floatingpanellayout-protocol) - [Change the initial position and height](#change-the-initial-position-and-height) - [Support your landscape layout](#support-your-landscape-layout) @@ -209,6 +210,16 @@ fpc.hide(animated: true) { NOTE: `FloatingPanelController` wraps `show`/`hide` with `addPanel`/`removePanelFromParent` for easy-to-use. But `show`/`hide` are more convenience for your app. +### Scale the content view when the surface position changes + +Specify the `contentMode` to `.fitToBounds` if the surface height fits the bounds of `FloatingPanelController.view` when the surface position changes + +```swift +fpc.contentMode = .fitToBounds +``` + +Otherwise, `FloatingPanelController` fixes the content by the height of the top most position. + ### Customize the layout with `FloatingPanelLayout` protocol #### Change the initial position and height From 6a86756dfa54a2167f60b967003e1ce011e474d4 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Wed, 7 Aug 2019 20:05:08 +0900 Subject: [PATCH 358/623] Fix not calling floatingPanelDidEndDecelerating delegate after interruption If the decelerating animation is interrupted and floatingPanelShouldBeginDragging delegate method returns false, floatingPanelDidEndDecelerating delegate method will not be called after calling floatingPanelWillBeginDecelerating method. A panel have to run an animation after the interruption and also floatingPanelDidEndDecelerating(_:) delegate should be called always after calling floatingPanelWillBeginDecelerating method. Therefore floatingPanelShouldBeginDragging delegate method shouldn't be called in the panel decelerating. --- Framework/Sources/FloatingPanel.swift | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Framework/Sources/FloatingPanel.swift b/Framework/Sources/FloatingPanel.swift index 7070fed4..8d338186 100644 --- a/Framework/Sources/FloatingPanel.swift +++ b/Framework/Sources/FloatingPanel.swift @@ -337,6 +337,11 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate { log.debug("panel gesture(\(state):\(panGesture.state)) --", "translation = \(translation.y), location = \(location.y), velocity = \(velocity.y)") + if interactionInProgress == false, isDecelerating == false, + let vc = viewcontroller, vc.delegate?.floatingPanelShouldBeginDragging(vc) == false { + return + } + if let animator = self.animator { guard surfaceView.presentationFrame.minY >= layoutAdapter.topMaxY else { return } log.debug("panel animation interrupted!!!") @@ -354,12 +359,6 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate { } } - if interactionInProgress == false, - let vc = viewcontroller, - vc.delegate?.floatingPanelShouldBeginDragging(vc) == false { - return - } - if panGesture.state == .began { panningBegan(at: location) return From 2fb88e3050e60dd7330516342a09011182cacc72 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Wed, 7 Aug 2019 22:26:52 +0900 Subject: [PATCH 359/623] Always call startInteraction before endInteraction --- Framework/Sources/FloatingPanel.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Framework/Sources/FloatingPanel.swift b/Framework/Sources/FloatingPanel.swift index 8d338186..65708226 100644 --- a/Framework/Sources/FloatingPanel.swift +++ b/Framework/Sources/FloatingPanel.swift @@ -375,6 +375,9 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate { } panningChange(with: translation) case .ended, .cancelled, .failed: + if interactionInProgress == false { + startInteraction(with: translation, at: location) + } panningEnd(with: translation, velocity: velocity) default: break From 4bdb3535a03c763f8bf9044fb92b0ee4aa97c5c8 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Thu, 8 Aug 2019 09:38:44 +0900 Subject: [PATCH 360/623] Fix stopping a panel b/w anchors after an interruption The panel(surface view) could stop b/w anchors if the pan gesture doesn't pass through `.changed` state after an interruptible animator is interrupted. The possible reason is the constraints have never changed since the last animation is committed so that `surfaceView.superview!.layoutIfNeeded()` doesn't trigger a layout update by the constraint-based layout system in `FloatingPanelLayoutAdapter.activateLayout(of:)`. Thus the inserted code changes a panel interactive constraint by the least positive number. It allows the constraint-based layout system to update the surface layout expectedly. --- Framework/Sources/FloatingPanel.swift | 8 +++++++- Framework/Sources/FloatingPanelLayout.swift | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Framework/Sources/FloatingPanel.swift b/Framework/Sources/FloatingPanel.swift index 65708226..6bb45b6f 100644 --- a/Framework/Sources/FloatingPanel.swift +++ b/Framework/Sources/FloatingPanel.swift @@ -344,7 +344,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate { if let animator = self.animator { guard surfaceView.presentationFrame.minY >= layoutAdapter.topMaxY else { return } - log.debug("panel animation interrupted!!!") + log.debug("panel animation(interruptible: \(animator.isInterruptible)) interrupted!!!") if animator.isInterruptible { animator.stopAnimation(false) // A user can stop a panel at the nearest Y of a target position so this fine-tunes @@ -377,6 +377,12 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate { case .ended, .cancelled, .failed: if interactionInProgress == false { startInteraction(with: translation, at: location) + // Workaround: Prevent stopping the surface view b/w anchors if the pan gesture + // doesn't pass through .changed state after an interruptible animator is interrupted. + let dy = translation.y - .leastNonzeroMagnitude + layoutAdapter.updateInteractiveTopConstraint(diff: dy, + allowsTopBuffer: true, + with: behavior) } panningEnd(with: translation, velocity: velocity) default: diff --git a/Framework/Sources/FloatingPanelLayout.swift b/Framework/Sources/FloatingPanelLayout.swift index bd49f49b..e2bb10ca 100644 --- a/Framework/Sources/FloatingPanelLayout.swift +++ b/Framework/Sources/FloatingPanelLayout.swift @@ -454,6 +454,7 @@ class FloatingPanelLayoutAdapter { func activateLayout(of state: FloatingPanelPosition) { defer { surfaceView.superview!.layoutIfNeeded() + log.debug("activateLayout -- surface.presentation = \(self.surfaceView.presentationFrame) surface.frame = \(self.surfaceView.frame)") } var state = state From 1596b2607f2b00d3b62a81ddaf53a9d277c0dfd2 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Fri, 9 Aug 2019 09:55:04 +0900 Subject: [PATCH 361/623] Release v1.6.4 --- FloatingPanel.podspec | 2 +- Framework/Sources/Info.plist | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/FloatingPanel.podspec b/FloatingPanel.podspec index cfb6274f..b0df0113 100644 --- a/FloatingPanel.podspec +++ b/FloatingPanel.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "FloatingPanel" - s.version = "1.6.3" + s.version = "1.6.4" s.summary = "FloatingPanel is a clean and easy-to-use UI component of a floating panel interface." s.description = <<-DESC FloatingPanel is a clean and easy-to-use UI component for a new interface introduced in Apple Maps, Shortcuts and Stocks app. diff --git a/Framework/Sources/Info.plist b/Framework/Sources/Info.plist index 62f15477..82a9d35d 100644 --- a/Framework/Sources/Info.plist +++ b/Framework/Sources/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 1.6.3 + 1.6.4 CFBundleVersion $(CURRENT_PROJECT_VERSION) From b2c33bf339f7962c1fdd9e42f0f3ab1f0209ae83 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Tue, 13 Aug 2019 15:27:27 +0900 Subject: [PATCH 362/623] Return true for FloatingPanelSurfaceView.requiresConstraintBasedLayout --- Framework/Sources/FloatingPanelSurfaceView.swift | 2 ++ Framework/Tests/FloatingPanelSurfaceViewTests.swift | 1 + 2 files changed, 3 insertions(+) diff --git a/Framework/Sources/FloatingPanelSurfaceView.swift b/Framework/Sources/FloatingPanelSurfaceView.swift index 876220d2..7f09951f 100644 --- a/Framework/Sources/FloatingPanelSurfaceView.swift +++ b/Framework/Sources/FloatingPanelSurfaceView.swift @@ -115,6 +115,8 @@ public class FloatingPanelSurfaceView: UIView { private lazy var grabberHandleHeightConstraint: NSLayoutConstraint = grabberHandle.heightAnchor.constraint(equalToConstant: grabberHandleHeight) private lazy var grabberHandleTopConstraint: NSLayoutConstraint = grabberHandle.topAnchor.constraint(equalTo: topAnchor, constant: grabberTopPadding) + public override class var requiresConstraintBasedLayout: Bool { return true } + override init(frame: CGRect) { super.init(frame: frame) addSubViews() diff --git a/Framework/Tests/FloatingPanelSurfaceViewTests.swift b/Framework/Tests/FloatingPanelSurfaceViewTests.swift index be5277da..8fb9179c 100644 --- a/Framework/Tests/FloatingPanelSurfaceViewTests.swift +++ b/Framework/Tests/FloatingPanelSurfaceViewTests.swift @@ -12,6 +12,7 @@ class FloatingPanelSurfaceViewTests: XCTestCase { func test_surfaceView() { let surface = FloatingPanelSurfaceView(frame: CGRect(x: 0.0, y: 0.0, width: 320.0, height: 480.0)) + XCTAssertTrue(FloatingPanelSurfaceView.requiresConstraintBasedLayout) XCTAssert(surface.contentView == nil) surface.layoutIfNeeded() XCTAssert(surface.grabberHandle.frame.minY == 6.0) From b76080c0150192689c70e963a41b745a14a8aa70 Mon Sep 17 00:00:00 2001 From: Yuki Yoshioka Date: Mon, 19 Aug 2019 14:34:11 +0900 Subject: [PATCH 363/623] Call floatingPanelDidEndRemove when dismiss with tap on backdrop view By #205 added dismissing by backdrop view. But `floatingPanelDidEndRemove` is not called from it. --- Framework/Sources/FloatingPanel.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Framework/Sources/FloatingPanel.swift b/Framework/Sources/FloatingPanel.swift index bf42f1c1..ddd45c1f 100644 --- a/Framework/Sources/FloatingPanel.swift +++ b/Framework/Sources/FloatingPanel.swift @@ -264,7 +264,10 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate { // MARK: - Gesture handling @objc func handleBackdrop(tapGesture: UITapGestureRecognizer) { - viewcontroller?.dismiss(animated: true, completion: nil) + viewcontroller?.dismiss(animated: true) { [weak self] in + guard let vc = self?.viewcontroller else { return } + vc.delegate?.floatingPanelDidEndRemove(vc) + } } @objc func handle(panGesture: UIPanGestureRecognizer) { From 69ab2200f2b4ce852ca5f09264b6de34054b49aa Mon Sep 17 00:00:00 2001 From: David Hart Date: Wed, 21 Aug 2019 08:43:44 +0200 Subject: [PATCH 364/623] Improve floatingPanelDidChangePosition and tigger it on removal --- Framework/Sources/FloatingPanel.swift | 1 + Framework/Sources/FloatingPanelController.swift | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Framework/Sources/FloatingPanel.swift b/Framework/Sources/FloatingPanel.swift index ddd45c1f..903bc39e 100644 --- a/Framework/Sources/FloatingPanel.swift +++ b/Framework/Sources/FloatingPanel.swift @@ -640,6 +640,7 @@ class FloatingPanel: NSObject, UIGestureRecognizerDelegate { let animator = behavior.removalInteractionAnimator(vc, with: velocityVector) animator.addAnimations { [weak self] in + self?.state = .hidden self?.updateLayout(to: .hidden) } animator.addCompletion({ _ in diff --git a/Framework/Sources/FloatingPanelController.swift b/Framework/Sources/FloatingPanelController.swift index e22f0541..02aa701f 100644 --- a/Framework/Sources/FloatingPanelController.swift +++ b/Framework/Sources/FloatingPanelController.swift @@ -12,7 +12,9 @@ public protocol FloatingPanelControllerDelegate: class { // if it returns nil, FloatingPanelController uses the default behavior func floatingPanel(_ vc: FloatingPanelController, behaviorFor newCollection: UITraitCollection) -> FloatingPanelBehavior? - func floatingPanelDidChangePosition(_ vc: FloatingPanelController) // changed the settled position in the model layer + /// Called when the floating panel has changed to a new position. Can be called inside an animation block, so any + /// view properties set inside this function will be automatically animated alongside the panel. + func floatingPanelDidChangePosition(_ vc: FloatingPanelController) /// Asks the delegate if dragging should begin by the pan gesture recognizer. func floatingPanelShouldBeginDragging(_ vc: FloatingPanelController) -> Bool From 887a4de2c318df46dea0cda875e879c05befb74b Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Wed, 14 Aug 2019 07:13:38 +0900 Subject: [PATCH 365/623] Fix UISearchBar's _searchField access --- Examples/Maps/Maps/ViewController.swift | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/Examples/Maps/Maps/ViewController.swift b/Examples/Maps/Maps/ViewController.swift index 62678509..c2ceab69 100644 --- a/Examples/Maps/Maps/ViewController.swift +++ b/Examples/Maps/Maps/ViewController.swift @@ -148,11 +148,9 @@ class SearchPanelViewController: UIViewController, UITableViewDataSource, UITabl tableView.dataSource = self tableView.delegate = self searchBar.placeholder = "Search for a place or address" - let textField = searchBar.value(forKey: "_searchField") as! UITextField - textField.font = UIFont(name: textField.font!.fontName, size: 15.0) + searchBar.setSearchText(fontSize: 15.0) hideHeader() - } override func viewDidLayoutSubviews() { @@ -274,3 +272,15 @@ class SearchHeaderView: UIView { self.clipsToBounds = true } } + +extension UISearchBar { + func setSearchText(fontSize: CGFloat) { + #if swift(>=5.1) // Xcode 11 or later + let font = searchTextField.font + searchTextField.font = font?.withSize(fontSize) + #else + let textField = value(forKey: "_searchField") as! UITextField + textField.font = textField.font?.withSize(fontSize) + #endif + } +} From c9c17053930b78b212f5295c965db6a12a01c17d Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Fri, 16 Aug 2019 14:08:30 +0900 Subject: [PATCH 366/623] Update README for UISearchController issue --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 861a9204..58f5caad 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ The new interface displays the related contents and utilities in parallel as a u - [Work your contents together with a floating panel behavior](#work-your-contents-together-with-a-floating-panel-behavior) - [Notes](#notes) - ['Show' or 'Show Detail' Segues from `FloatingPanelController`'s content view controller](#show-or-show-detail-segues-from-floatingpanelcontrollers-content-view-controller) + - [UISearchController issue](#uisearchcontroller-issue) - [FloatingPanelSurfaceView's issue on iOS 10](#floatingpanelsurfaceviews-issue-on-ios-10) - [Author](#author) - [License](#license) @@ -439,6 +440,12 @@ A `FloatingPanelController` object proxies an action for `show(_:sender)` to the It's a great way to decouple between a floating panel and the content VC. +### UISearchController issue + +`UISearchController` isn't able to be used with `FloatingPanelController` by the system design. + +Because `UISearchController` automatically presents itself modally when a user interacts with the search bar, and then it swaps the superview of the search bar to the view managed by itself while it displays. As a result, `FloatingPanelController` can't control the search bar when it's active, as you can see from [the screen shot](https://github.com/SCENEE/FloatingPanel/issues/248#issuecomment-521263831). + ### FloatingPanelSurfaceView's issue on iOS 10 * On iOS 10, `FloatingPanelSurfaceView.cornerRadius` isn't not automatically masked with the top rounded corners because of `UIVisualEffectView` issue. See https://forums.developer.apple.com/thread/50854. From 6c041fb907ef8b6405050d9a7a3565ef7d6b7519 Mon Sep 17 00:00:00 2001 From: Shin Yamamoto Date: Fri, 16 Aug 2019 14:21:50 +0900 Subject: [PATCH 367/623] Add move-to-hidden tests --- .../Samples/Sources/Base.lproj/Main.storyboard | 11 +++++++++-- Examples/Samples/Sources/ViewController.swift | 4 +++- .../Tests/FloatingPanelControllerTests.swift | 18 ++++++++++++++++-- Framework/Tests/Utils.swift | 4 ++++ 4 files changed, 32 insertions(+), 5 deletions(-) diff --git a/Examples/Samples/Sources/Base.lproj/Main.storyboard b/Examples/Samples/Sources/Base.lproj/Main.storyboard index f8c45dd9..47ac5bd3 100644 --- a/Examples/Samples/Sources/Base.lproj/Main.storyboard +++ b/Examples/Samples/Sources/Base.lproj/Main.storyboard @@ -324,7 +324,7 @@ - + + - + + - @@ -199,7 +210,7 @@ - + - @@ -230,7 +241,7 @@ - + - @@ -262,20 +273,19 @@ + - - + - @@ -314,7 +324,7 @@ - - - - - - + - + @@ -555,14 +565,14 @@ - - - - + - + - +