NOTE: This is beta! Pull-requests are welcome. I've archived this library's repository because I no longer have access to the code of the project with which I could fully test/validate its functionality. Feel free to fork this and use it as a baseline for your own varients! But, please credit this original repo as a point of reference.
Uses UIPercentDrivenInteractiveTransition
and UIViewControllerAnimatedTransitioning
to quickly implement nice “pull-to-dismiss” interactions on modal view controller(s). — Also handles UIScrollView
bounce toggling dynamically when necessary.
More about the origins of this project here: https://benguild.com/2018/09/03/simple-pull-to-dismiss-uiscrollview-swift/
There are 3 bundled transition types:
slideStatic
= Default, to slide away vertically with a panning gesture.slideDynamic
= Same asslideStatic
, but does not screenshot the view controller.scale
= Scales down the view before eventually fading it out and fading in the view underneath.
All transition types are configured to provide minimal interference with the vertical "bouncing" of a UIScrollView
.
Example of the “slide” transition reversing against a UIScrollView
in a single, fluid gesture:
PullToDismissTransition
is available through CocoaPods. To install
it, simply add the following line to your Podfile:
pod "PullToDismissTransition"
When presenting your modal view controller, be sure to set the presentation context, or the transitions will not appear over other view controllers below:
var viewController = MyAwesomeViewController()
viewController.isPullToDismissEnabled = true
viewController.modalPresentationCapturesStatusBarAppearance = true
viewController.modalPresentationStyle = .overFullScreen
self.present(viewController, animated: true)
The simplest implementation is through adopting the PullToDismissable
protocol on your view controller(s):
import PullToDismissTransition
import UIKit
class MyAwesomeViewController: UIViewController, PullToDismissable {
private(set) lazy var pullToDismissTransition: PullToDismissTransition = {
let pullToDismissTransition = PullToDismissTransition(
viewController: self,
transitionType: .slideStatic
)
////
// NOTE: Optional, unless you implement any of the delegate methods:
pullToDismissTransition.delegate = self
return pullToDismissTransition
}()
override func didMove(toParentViewController parent: UIViewController?) {
super.didMove(toParentViewController: parent)
setupPullToDismiss()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
setupPullToDismiss()
////
// NOTE: Optional, unless you've navigated to a scroll-view within a navigation
// flow (but the same context), and therefore must toggle monitoring to it:
pullToDismissTransition.monitorActiveScrollView(scrollView: scrollView)
}
}
extension MyAwesomeViewController: UIViewControllerTransitioningDelegate {
func animationController(forDismissed dismissed: UIViewController)
-> UIViewControllerAnimatedTransitioning? {
guard isPullToDismissEnabled else { return nil }
return pullToDismissTransition
}
func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning)
-> UIViewControllerInteractiveTransitioning? {
guard isPullToDismissEnabled else { return nil }
return pullToDismissTransition
}
}
... Ta-da!
Use the setPullToDismissEnabled()
method to revert back to the default transition(s) in iOS, such as when you're dismissing via a button press. You can also call this method within viewDidLoad()
to enable if you didn't set "isPullToDismissEnabled" at time of initialization.
To gain additional control, optionally implement the delegate of PullToDismissTransition
:
extension MyAwesomeViewController: PullToDismissTransitionDelegate {
func canBeginPullToDismiss(on dismissingViewController: UIViewController) -> Bool {
// Return "false" to disable the transition during certain events, such as a horizontal
// page-swipe.
return true
}
func didBeginPullToDismissAttempt(on dismissingViewController: UIViewController) {
guard dismissingViewController === self else { return }
// Handle events when the transition becomes active, such as adjusting or disabling certain GUI.
}
func didCompletePullToDismissAttempt(on dismissingViewController: UIViewController, willDismiss: Bool) {
guard dismissingViewController === self else { return }
// Called when the attempt is cancelled or completed.
// Handle events when the transition becomes active, such as reverting adjusted or re-enabling
// certain GUI.
}
func didFinishTransition(for dismissingViewController: UIViewController, didDismiss: Bool) {
guard dismissingViewController === self else { return }
// Called after the transition completes.
}
}
None of these methods are required in your implementation, but are available in case you need them.
PullToDismissTransition
is designed to work with UIScrollView
, and provide minimal interference with any vertical bouncing. However, when changing between view controllers within the same context (such as when navigating within view controllers in UINavigationController
), you need to keep the transition up-to-date with whichever scroll view is actively displayed on-screen:
pullToDismissTransition.monitorActiveScrollView(scrollView: myAwesomeScrollView)
Calling this method will replace any existing observation, and should also be used for other scroll view descendants such as UITableView
, UICollectionView
, etc.
The PullToDismissable
protocol is designed for use on the child view-controller(s) of a UINavigationController
, as views within the navigation flow may have scrolling/bouncing behaviors that require monitoring via the transition's monitorActiveScrollView(scrollView: UIScrollView)
method.
While the PullToDismissable
protocol is provided for your convenience and to reduce code complexity in re-use, it is also fine to implement PullToDismissTransition
yourself directly by referencing this protocol's code for the transition's integration.
Ben Guild, hello@benguild.com
PullToDismissTransition
is available under the MIT license. See the LICENSE file for more info.