Add support for scroll views inside of navigation controllers
This commit is contained in:
parent
c34938a03d
commit
f79d5a6b59
|
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,26 +127,24 @@ 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:
|
||||||
|
@ -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()
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue