问题
I have been experimenting with custom interactive view controller presentation and dismissal (using a combination of UIPresentationController
, UIPercentDrivenInteractiveTransition
, UIViewControllerAnimatedTransitioning
, and UIViewControllerTransitioningDelegate
) and have mostly gotten things working well for my needs.
However, there is one common scenario that I've yet to find addressed in any of the tutorials or documentation that I've read, leading me to the following question:
...
What is the proper way of handling custom interactive view controller dismissal, via a pan gesture, when the dismissed view contains a UIScrollView (ie. UITableView, UICollectionView, WKWebView, etc)?
...
Basically, what I'd like is for the following:
View controllers are interactively dismissible by panning them down. This is common UX in many apps.
If the dismissed view controller contains a (vertically-scrolling) scroll view, panning down scrolls that view as expected until the user reaches the top, after which the scrolling ceases and the pan-to-dismiss occurs.
Scroll views should otherwise behave as normal.
I know that this is technically possible - I've seen it in other apps, such as Overcast and Apple's own Music app - but I've not been able to find the key to coordinating the behavior of my pan gesture with that of the scroll view(s).
Most of my own attempts center on trying to conditionally enable/disable the scrollview (or its associated pan gesture recognizer) based on its contentOffset.y
while scrolling and having the view controller dismissal's pan gesture recognizer take over from there, but this has been fraught with problems and I fear that I am overthinking it.
I feel like the secret mostly lies in the following pan gesture recognizer delegate method:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
// ...
}
I have created a reduced sample project which should demonstrate the scenario more clearly. Any code suggestions are highly welcome!
https://github.com/Darchmare/SlidePanel-iOS
回答1:
Solution
Make
scrollView
stop scrolling after it reached top by using UIScrollView's bounces property and scrollViewDidScroll(_:) method.func scrollViewDidScroll(_ scrollView: UIScrollView) { scrollView.bounces = (scrollView.contentOffset.y > 10); }
Don't forget to set
scrollView.delegate = self
Only handle
panGestureRecognizer
whenscrollView
reached top - It means whenscrollView.contentOffset.y == 0
by using a protocol.protocol PanelAnimationControllerDelegate { func shouldHandlePanelInteractionGesture() -> Bool }
ViewController
func shouldHandlePanelInteractionGesture() -> Bool { return (scrollView.contentOffset.y == 0); }
PanelInteractionController
class PanelInteractionController: ... { var startY:CGFloat = 0 private weak var viewController: (UIViewController & PanelAnimationControllerDelegate)? @objc func handlePanGestureRecognizer(_ gestureRecognizer: UIPanGestureRecognizer) { switch gestureRecognizer.state { case .began: break case .changed: let translation = gestureRecognizer.translation(in: gestureRecognizer.view!.superview!) let velocity = gestureRecognizer.velocity(in: gestureRecognizer.view!.superview) let state = gestureRecognizer.state // Don't do anything when |scrollView| is scrolling if !(viewController?.shouldHandlePanelInteractionGesture())! && percentComplete == 0 { return; } var rawProgress = CGFloat(0.0) rawProgress = ((translation.y - startTransitionY) / gestureRecognizer.view!.bounds.size.height) let progress = CGFloat(fminf(fmaxf(Float(rawProgress), 0.0), 1.0)) if abs(velocity.x) > abs(velocity.y) && state == .began { // If the user attempts a pan and it looks like it's going to be mostly horizontal, bail - we don't want it... - JAC return } if !self.interactionInProgress { // Start to pan |viewController| down self.interactionInProgress = true startTransitionY = translation.y; self.viewController?.dismiss(animated: true, completion: nil) } else { // If the user gets to a certain point within the dismissal and releases the panel, allow the dismissal to complete... - JAC self.shouldCompleteTransition = progress > 0.2 update(progress) } case .cancelled: self.interactionInProgress = false startTransitionY = 0 cancel() case .ended: self.interactionInProgress = false startTransitionY = 0 if self.shouldCompleteTransition == false { cancel() } else { finish() } case .failed: self.interactionInProgress = false startTransitionY = 0 cancel() default: break; } } }
Result
For more detail, you can take a look at my sample project
回答2:
For me, this little bit of code answered a lot of my issues and greatly helped my custom transitions in scrollviews, it will hold a negative scrollview offset from moving while trying to start a transition or showing an activity indicator on the top. My guess is that this will solve at least some of your transition/animation hiccups:
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if scrollView.contentOffset.y < -75 {
scrollView.contentInset.top = -scrollView.contentOffset.y
}
// Do animation or transition
}
来源:https://stackoverflow.com/questions/50011682/handling-scroll-views-with-custom-interactive-view-controller-presentation-an