Add support for scroll views inside of navigation controllers

This commit is contained in:
Shadowfacts 2019-12-31 23:04:48 -05:00
parent c34938a03d
commit f79d5a6b59
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
4 changed files with 115 additions and 22 deletions

View File

@ -11,6 +11,8 @@ import UIKit
public protocol SheetContainerViewControllerDelegate { public protocol SheetContainerViewControllerDelegate {
func sheetContainer(_ sheetContainer: SheetContainerViewController, willSnapToDetent detent: Detent) -> Bool func sheetContainer(_ sheetContainer: SheetContainerViewController, willSnapToDetent detent: Detent) -> Bool
func sheetContainer(_ sheetContainer: SheetContainerViewController, didSnapToDetent detent: Detent) func sheetContainer(_ sheetContainer: SheetContainerViewController, didSnapToDetent detent: Detent)
func sheetContainerContentScrollView(_ sheetContainer: SheetContainerViewController) -> UIScrollView?
func sheetContainer(_ sheetContainer: SheetContainerViewController, topContentOffsetForScrollView scrollView: UIScrollView) -> CGFloat
} }
// default no-op implementation // default no-op implementation
@ -19,13 +21,19 @@ public extension SheetContainerViewControllerDelegate {
return true return true
} }
func sheetContainer(_ sheetContainer: SheetContainerViewController, didSnapToDetent detent: Detent) {} func sheetContainer(_ sheetContainer: SheetContainerViewController, didSnapToDetent detent: Detent) {}
func sheetContainerContentScrollView(_ sheetContainer: SheetContainerViewController) -> UIScrollView? {
return nil
}
func sheetContainer(_ sheetContainer: SheetContainerViewController, topContentOffsetForScrollView scrollView: UIScrollView) -> CGFloat {
return 0
}
} }
public class SheetContainerViewController: UIViewController { public class SheetContainerViewController: UIViewController {
public var delegate: SheetContainerViewControllerDelegate? public var delegate: SheetContainerViewControllerDelegate?
let content: UIViewController public let content: UIViewController
public var detents: [Detent] = [.bottom, .middle, .top] public var detents: [Detent] = [.bottom, .middle, .top]
var topDetent: (detent: Detent, offset: CGFloat) { var topDetent: (detent: Detent, offset: CGFloat) {
@ -44,7 +52,10 @@ public class SheetContainerViewController: UIViewController {
var dimmingView: UIView! var dimmingView: UIView!
public var minimumDimmingAlpha: CGFloat = 0 public var minimumDimmingAlpha: CGFloat = 0
public var maximumDimmingAlpha: CGFloat = 0.75 public var maximumDimmingAlpha: CGFloat = 0.75
var initialScrollViewContentOffset = CGPoint.zero
var contentScrollView: UIScrollView? {
delegate?.sheetContainerContentScrollView(self) ?? content.view as? UIScrollView
}
public init(content: UIViewController) { public init(content: UIViewController) {
self.content = content self.content = content
@ -85,11 +96,12 @@ public class SheetContainerViewController: UIViewController {
dimmingView.bottomAnchor.constraint(equalTo: content.view.topAnchor, constant: content.view.layer.cornerRadius) dimmingView.bottomAnchor.constraint(equalTo: content.view.topAnchor, constant: content.view.layer.cornerRadius)
]) ])
if let scrollView = content.view as? UIScrollView { let panGesture = UIPanGestureRecognizer(target: self, action: #selector(panGestureRecognized(_:)))
panGesture.delegate = self
content.view.addGestureRecognizer(panGesture)
if let scrollView = contentScrollView {
scrollView.panGestureRecognizer.addTarget(self, action: #selector(scrollViewPanGestureRecognized)) scrollView.panGestureRecognizer.addTarget(self, action: #selector(scrollViewPanGestureRecognized))
} else {
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(panGestureRecognized(_:)))
content.view.addGestureRecognizer(panGesture)
} }
} }
@ -114,29 +126,27 @@ public class SheetContainerViewController: UIViewController {
@objc func scrollViewPanGestureRecognized(_ recognizer: UIPanGestureRecognizer) { @objc func scrollViewPanGestureRecognized(_ recognizer: UIPanGestureRecognizer) {
guard let scrollView = recognizer.view as? UIScrollView else { return } guard let scrollView = recognizer.view as? UIScrollView else { return }
let translation = recognizer.translation(in: scrollView)
let velocity = recognizer.velocity(in: scrollView) let velocity = recognizer.velocity(in: scrollView)
let shouldMoveSheetDown = scrollView.contentOffset.y <= 0 && velocity.y > 0 // scrolled to top and dragging down let topContentOffset: CGFloat = delegate?.sheetContainer(self, topContentOffsetForScrollView: scrollView) ?? 0
let shouldMoveSheetDown = scrollView.contentOffset.y <= topContentOffset && velocity.y > 0 // scrolled to top and dragging down
let shouldMoveSheetUp = topConstraint.constant > topDetent.offset && velocity.y < 0 // not fully expanded and dragging up let shouldMoveSheetUp = topConstraint.constant > topDetent.offset && velocity.y < 0 // not fully expanded and dragging up
let shouldMoveSheet = shouldMoveSheetDown || shouldMoveSheetUp let shouldMoveSheet = shouldMoveSheetDown || shouldMoveSheetUp
if shouldMoveSheet { if shouldMoveSheet {
scrollView.bounces = false scrollView.bounces = false
scrollView.setContentOffset(.zero, animated: false) scrollView.setContentOffset(CGPoint(x: 0, y: topContentOffset), animated: false)
} }
switch recognizer.state { switch recognizer.state {
case .began:
initialScrollViewContentOffset = scrollView.contentOffset
case .changed: case .changed:
if shouldMoveSheet { if shouldMoveSheet {
let translation = recognizer.translation(in: scrollView)
setTopOffset(topConstraint.constant + translation.y) setTopOffset(topConstraint.constant + translation.y)
recognizer.setTranslation(initialScrollViewContentOffset, in: scrollView) recognizer.setTranslation(.zero, in: scrollView)
} }
case .ended: case .ended:
scrollView.bounces = true scrollView.bounces = true
if shouldMoveSheet { if shouldMoveSheet {
@ -205,6 +215,15 @@ public class SheetContainerViewController: UIViewController {
} }
extension SheetContainerViewController: UIGestureRecognizerDelegate {
public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if topConstraint.constant <= topDetent.offset {
return (gestureRecognizer as! UIPanGestureRecognizer).translation(in: gestureRecognizer.view!).y > 0
}
return true
}
}
extension SheetContainerViewController: UIViewControllerTransitioningDelegate { extension SheetContainerViewController: UIViewControllerTransitioningDelegate {
public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return SheetContainerPresentationAnimationController() return SheetContainerPresentationAnimationController()

View File

@ -16,29 +16,36 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="hIF-hV-InX"> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="hIF-hV-InX">
<rect key="frame" x="188.5" y="403" width="37" height="90"/> <rect key="frame" x="173" y="388" width="68" height="120"/>
<subviews> <subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="tfd-T0-fMO"> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="tfd-T0-fMO">
<rect key="frame" x="0.0" y="0.0" width="37" height="30"/> <rect key="frame" x="0.0" y="0.0" width="68" height="30"/>
<state key="normal" title="Plain"/> <state key="normal" title="Plain"/>
<connections> <connections>
<action selector="plainPressed:" destination="BYZ-38-t0r" eventType="touchUpInside" id="tmi-VL-j61"/> <action selector="plainPressed:" destination="BYZ-38-t0r" eventType="touchUpInside" id="tmi-VL-j61"/>
</connections> </connections>
</button> </button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="fT8-yG-J2R"> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="fT8-yG-J2R">
<rect key="frame" x="0.0" y="30" width="37" height="30"/> <rect key="frame" x="0.0" y="30" width="68" height="30"/>
<state key="normal" title="Table"/> <state key="normal" title="Table"/>
<connections> <connections>
<action selector="tablePressed:" destination="BYZ-38-t0r" eventType="touchUpInside" id="PZl-oC-0t4"/> <action selector="tablePressed:" destination="BYZ-38-t0r" eventType="touchUpInside" id="PZl-oC-0t4"/>
</connections> </connections>
</button> </button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="39x-b2-4Hx"> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="39x-b2-4Hx">
<rect key="frame" x="0.0" y="60" width="37" height="30"/> <rect key="frame" x="0.0" y="60" width="68" height="30"/>
<state key="normal" title="Nav"/> <state key="normal" title="Nav"/>
<connections> <connections>
<action selector="navPressed:" destination="BYZ-38-t0r" eventType="touchUpInside" id="Jc9-R9-VZW"/> <action selector="navPressed:" destination="BYZ-38-t0r" eventType="touchUpInside" id="Jc9-R9-VZW"/>
</connections> </connections>
</button> </button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="eUF-7N-Qgr">
<rect key="frame" x="0.0" y="90" width="68" height="30"/>
<state key="normal" title="Nav Table"/>
<connections>
<action selector="navTablePressed:" destination="BYZ-38-t0r" eventType="touchUpInside" id="fSY-4h-jE3"/>
</connections>
</button>
</subviews> </subviews>
</stackView> </stackView>
</subviews> </subviews>

View File

@ -40,4 +40,21 @@ class ContentTableViewController: UITableViewController {
return cell return cell
} }
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let navController = navigationController else { return }
let vc = UIViewController()
vc.view.backgroundColor = .systemBackground
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.text = "\(indexPath.row)"
vc.view.addSubview(label)
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: vc.view.centerXAnchor),
label.centerYAnchor.constraint(equalTo: vc.view.centerYAnchor)
])
navController.pushViewController(vc, animated: true)
}
} }

View File

@ -58,6 +58,42 @@ class ViewController: UIViewController {
} }
@IBAction func navPressed(_ sender: Any) { @IBAction func navPressed(_ sender: Any) {
let root = UIViewController()
root.view.backgroundColor = .systemBackground
let label = UILabel()
label.text = "Root VC"
label.translatesAutoresizingMaskIntoConstraints = false
root.view.addSubview(label)
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: root.view.centerXAnchor),
label.centerYAnchor.constraint(equalTo: root.view.centerYAnchor)
])
let nav = UINavigationController(rootViewController: root)
nav.view.translatesAutoresizingMaskIntoConstraints = false
nav.view.layer.masksToBounds = true
nav.view.layer.cornerRadius = view.bounds.width * 0.02
let sheet = SheetContainerViewController(content: nav)
sheet.delegate = self
sheet.detents = [.bottom, .middle, .top]
present(sheet, animated: true)
}
@IBAction func navTablePressed(_ sender: Any) {
let table = ContentTableViewController()
let nav = UINavigationController(rootViewController: table)
nav.view.translatesAutoresizingMaskIntoConstraints = false
nav.view.layer.masksToBounds = true
nav.view.layer.cornerRadius = view.bounds.width * 0.02
let sheet = SheetContainerViewController(content: nav)
sheet.delegate = self
sheet.detents = [.bottom, .middle, .top]
present(sheet, animated: true)
} }
} }
@ -69,4 +105,18 @@ extension ViewController: SheetContainerViewControllerDelegate {
} }
return true return true
} }
func sheetContainerContentScrollView(_ sheetContainer: SheetContainerViewController) -> UIScrollView? {
if let navController = sheetContainer.content as? UINavigationController, let scrollView = navController.visibleViewController?.view as? UIScrollView {
return scrollView
} else {
return nil
}
}
func sheetContainer(_ sheetContainer: SheetContainerViewController, topContentOffsetForScrollView scrollView: UIScrollView) -> CGFloat {
if let navController = sheetContainer.content as? UINavigationController, navController.visibleViewController?.view is UIScrollView {
return -navController.navigationBar.bounds.height
} else {
return 0
}
}
} }