Skip to content

Commit

Permalink
Add floatingPanel(_:contentOffsetForPinning:) delegate method (#314)
Browse files Browse the repository at this point in the history
* add floatingPanel(_:contentOffsetForPinning:)
* add 'Show NavigationController' sample
* fix the initial content offset in a navigation bar with  a large text
    The content offset preservation should be applied only when
    `FloatingPanelController.contentInsetAdjustmentBehavior` is `.always`.
    This is because the library user loses control of the initial offset.
  • Loading branch information
scenee authored Feb 24, 2020
1 parent 1f79c25 commit 65f67c9
Show file tree
Hide file tree
Showing 5 changed files with 72 additions and 27 deletions.
10 changes: 4 additions & 6 deletions Examples/Samples/Sources/Base.lproj/Main.storyboard
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="RoN-h0-uBD">
<device id="retina5_9" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15705" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="RoN-h0-uBD">
<device id="retina5_9" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15706"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Navigation Controller-->
<scene sceneID="Cjh-iX-VQw">
<objects>
<navigationController id="RoN-h0-uBD" sceneMemberID="viewController">
<navigationController storyboardIdentifier="RootNavigationController" id="RoN-h0-uBD" sceneMemberID="viewController">
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="hNW-5m-Omi">
<rect key="frame" x="0.0" y="44" width="375" height="96"/>
<autoresizingMask key="autoresizingMask"/>
Expand Down
28 changes: 27 additions & 1 deletion Examples/Samples/Sources/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class SampleListViewController: UIViewController {
case showIntrinsicView
case showContentInset
case showContainerMargins
case showNavigationController

var name: String {
switch self {
Expand All @@ -42,6 +43,7 @@ class SampleListViewController: UIViewController {
case .showIntrinsicView: return "Show Intrinsic View"
case .showContentInset: return "Show with ContentInset"
case .showContainerMargins: return "Show with ContainerMargins"
case .showNavigationController: return "Show Navigation Controller"
}
}

Expand All @@ -60,6 +62,7 @@ class SampleListViewController: UIViewController {
case .showIntrinsicView: return "IntrinsicViewController"
case .showContentInset: return nil
case .showContainerMargins: return nil
case .showNavigationController: return "RootNavigationController"
}
}
}
Expand All @@ -80,6 +83,7 @@ class SampleListViewController: UIViewController {
tableView.dataSource = self
tableView.delegate = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
automaticallyAdjustsScrollViewInsets = false

let searchController = UISearchController(searchResultsController: nil)
if #available(iOS 11.0, *) {
Expand All @@ -92,11 +96,14 @@ class SampleListViewController: UIViewController {

let contentVC = DebugTableViewController()
addMainPanel(with: contentVC)

var insets = UIEdgeInsets.zero
insets.bottom += 69.0
tableView.contentInset = insets
}

override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)

if #available(iOS 11.0, *) {
if let observation = navigationController?.navigationBar.observe(\.prefersLargeTitles, changeHandler: { (bar, _) in
self.tableView.reloadData()
Expand All @@ -119,6 +126,7 @@ class SampleListViewController: UIViewController {
// Initialize FloatingPanelController
mainPanelVC = FloatingPanelController()
mainPanelVC.delegate = self
mainPanelVC.contentInsetAdjustmentBehavior = .always

// Initialize FloatingPanelController and add the view
mainPanelVC.surfaceView.cornerRadius = 6.0
Expand All @@ -143,6 +151,8 @@ class SampleListViewController: UIViewController {

let backdropTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleBackdrop(tapGesture:)))
mainPanelVC.backdropView.addGestureRecognizer(backdropTapGesture)
case .showNavigationController:
mainPanelVC.contentInsetAdjustmentBehavior = .never
default:
break
}
Expand All @@ -160,6 +170,11 @@ class SampleListViewController: UIViewController {
mainPanelVC.track(scrollView: contentVC.tableView)
case let contentVC as NestedScrollViewController:
mainPanelVC.track(scrollView: contentVC.scrollView)
case let navVC as UINavigationController:
if let rootVC = (navVC.topViewController as? SampleListViewController) {
rootVC.loadViewIfNeeded()
mainPanelVC.track(scrollView: rootVC.tableView)
}
default:
break
}
Expand Down Expand Up @@ -368,6 +383,14 @@ extension SampleListViewController: UITableViewDelegate {
}

extension SampleListViewController: FloatingPanelControllerDelegate {
func foatingPanel(_ vc: FloatingPanelController, contentOffsetForPinning trackedScrollView: UIScrollView) -> CGPoint {
if currentMenu == .showNavigationController, #available(iOSApplicationExtension 11.0, *) {
// 148.0 is the SafeArea's top value for a navigation bar with a large title.
return CGPoint(x: 0.0, y: 0.0 - trackedScrollView.contentInset.top - 148.0)
}
return CGPoint(x: 0.0, y: 0.0 - trackedScrollView.contentInset.top)
}

func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? {
if vc == settingsPanelVC {
return IntrinsicPanelLayout()
Expand Down Expand Up @@ -662,6 +685,9 @@ class DebugTableViewController: InspectableViewController {
])
tableView.dataSource = self
tableView.delegate = self
if #available(iOS 11.0, *) {
tableView.contentInsetAdjustmentBehavior = .never
}
self.tableView = tableView

let stackView = UIStackView()
Expand Down
23 changes: 20 additions & 3 deletions Framework/Sources/FloatingPanelController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ public protocol FloatingPanelControllerDelegate: class {
///
/// By default, any tap and long gesture recognizers are allowed to recognize gestures simultaneously.
func floatingPanel(_ vc: FloatingPanelController, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool

/// Asks the delegate for a content offset of the tracked scroll view to be pinned when a floating panel moves
///
/// If you do not implement this method, the controller uses a value of the content offset plus the content insets
/// of the tracked scroll view. Your implementation of this method can return a value for a navigation bar with a large
/// title, for example.
///
/// This method will not be called if the controller doesn't track any scroll view.
func foatingPanel(_ vc: FloatingPanelController, contentOffsetForPinning trackedScrollView: UIScrollView) -> CGPoint
}

public extension FloatingPanelControllerDelegate {
Expand All @@ -62,6 +71,9 @@ public extension FloatingPanelControllerDelegate {
func floatingPanel(_ vc: FloatingPanelController, shouldRecognizeSimultaneouslyWith gestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
func foatingPanel(_ vc: FloatingPanelController, contentOffsetForPinning trackedScrollView: UIScrollView) -> CGPoint {
return CGPoint(x: 0.0, y: 0.0 - trackedScrollView.contentInset.top)
}
}


Expand Down Expand Up @@ -381,13 +393,18 @@ open class FloatingPanelController: UIViewController {
private func activateLayout() {
floatingPanel.layoutAdapter.prepareLayout(in: self)

// preserve the current content offset
let contentOffset = scrollView?.contentOffset
// preserve the current content offset if contentInsetAdjustmentBehavior is `.always`
var contentOffset: CGPoint?
if contentInsetAdjustmentBehavior == .always {
contentOffset = scrollView?.contentOffset
}

floatingPanel.layoutAdapter.updateHeight()
floatingPanel.layoutAdapter.activateLayout(of: floatingPanel.state)

scrollView?.contentOffset = contentOffset ?? .zero
if let contentOffset = contentOffset {
scrollView?.contentOffset = contentOffset
}
}

// MARK: - Container view controller interface
Expand Down
29 changes: 17 additions & 12 deletions Framework/Sources/FloatingPanelCore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,6 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
}

public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
guard gestureRecognizer == panGestureRecognizer else { return false }
/* log.debug("shouldBeRequiredToFailBy", otherGestureRecognizer) */
return false
}
Expand All @@ -237,8 +236,7 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
if grabberAreaFrame.contains(gestureRecognizer.location(in: gestureRecognizer.view)) {
return false
}
let offset = scrollView.contentOffset.y - scrollView.contentOffsetZero.y
return allowScrollPanGesture(at: CGPoint(x: 0.0, y: offset))
return allowScrollPanGesture(for: scrollView)
default:
return false
}
Expand Down Expand Up @@ -293,14 +291,13 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
let surfaceMinY = surfaceView.presentationFrame.minY
let adapterTopY = layoutAdapter.topY
let belowTop = surfaceMinY > (adapterTopY + (1.0 / surfaceView.traitCollection.displayScale))
let offset = scrollView.contentOffset.y - scrollView.contentOffsetZero.y

log.debug("scroll gesture(\(state):\(panGesture.state)) --",
"belowTop = \(belowTop),",
"interactionInProgress = \(interactionInProgress),",
"scroll offset = \(offset),",
"scroll offset = \(scrollView.contentOffset.y),",
"location = \(location.y), velocity = \(velocity.y)")

let offset = scrollView.contentOffset.y - contentOrigin(of: scrollView).y

if belowTop {
// Scroll offset pinning
Expand Down Expand Up @@ -343,11 +340,11 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
} else {
if state == layoutAdapter.topMostState {
// Hide a scroll indicator just before starting an interaction by swiping a panel down.
if velocity.y > 0, !allowScrollPanGesture(at: CGPoint(x: 0.0, y: offset)) {
if velocity.y > 0, !allowScrollPanGesture(for: scrollView) {
lockScrollView()
}
// Show a scroll indicator when an animation is interrupted at the top and content is scrolled up
if velocity.y < 0, allowScrollPanGesture(at: CGPoint(x: 0.0, y: offset)) {
if velocity.y < 0, allowScrollPanGesture(for: scrollView) {
unlockScrollView()
}

Expand Down Expand Up @@ -457,7 +454,7 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
return false
}

let offset = scrollView.contentOffset.y - scrollView.contentOffsetZero.y
let offset = scrollView.contentOffset.y - contentOrigin(of: scrollView).y
// The zero offset must be excluded because the offset is usually zero
// after a panel moves from half/tip to full.
if offset > 0.0 {
Expand Down Expand Up @@ -686,9 +683,9 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
if grabberAreaFrame.contains(location) || scrollView.isTracking == false {
initialScrollOffset = scrollView.contentOffset
} else {
initialScrollOffset = scrollView.contentOffsetZero
initialScrollOffset = contentOrigin(of: scrollView)
// Fit the surface bounds to a scroll offset content by startInteraction(at:offset:)
let scrollOffsetY = (scrollView.contentOffset.y - scrollView.contentOffsetZero.y)
let scrollOffsetY = (scrollView.contentOffset.y - contentOrigin(of: scrollView).y)
if scrollOffsetY < 0 {
offset = CGPoint(x: -scrollView.contentOffset.x, y: -scrollOffsetY)
}
Expand Down Expand Up @@ -889,7 +886,15 @@ class FloatingPanelCore: NSObject, UIGestureRecognizerDelegate {
scrollView?.setContentOffset(contentOffset, animated: false)
}

private func allowScrollPanGesture(at contentOffset: CGPoint) -> Bool {
private func contentOrigin(of scrollView: UIScrollView) -> CGPoint {
if let vc = viewcontroller, let origin = vc.delegate?.foatingPanel(vc, contentOffsetForPinning: scrollView) {
return origin
}
return CGPoint(x: 0.0, y: 0.0 - scrollView.contentInset.top)
}

private func allowScrollPanGesture(for scrollView: UIScrollView) -> Bool {
let contentOffset = scrollView.contentOffset - contentOrigin(of: scrollView)
if state == layoutAdapter.topMostState {
return contentOffset.y <= -30.0 || contentOffset.y > 0
}
Expand Down
9 changes: 4 additions & 5 deletions Framework/Sources/UIExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,6 @@ extension UIGestureRecognizerState: CustomDebugStringConvertible {
#endif

extension UIScrollView {
var contentOffsetZero: CGPoint {
return CGPoint(x: 0.0, y: 0.0 - contentInset.top)
}
var isLocked: Bool {
return !showsVerticalScrollIndicator && !bounces && isDirectionalLockEnabled
}
Expand All @@ -133,8 +130,10 @@ extension UISpringTimingParameters {

extension CGPoint {
static var nan: CGPoint {
return CGPoint(x: CGFloat.nan,
y: CGFloat.nan)
return CGPoint(x: CGFloat.nan, y: CGFloat.nan)
}
static func - (left: CGPoint, right: CGPoint) -> CGPoint {
return CGPoint(x: left.x - right.x, y: left.y - right.y)
}
}

Expand Down

0 comments on commit 65f67c9

Please sign in to comment.