在 iOS 中,如何拖放一个模态?

一个常见的方法来取消模态是向下滑动-我们如何允许用户拖动模态下来,如果它足够远,模态被取消,否则它动画回到原来的位置?

例如,我们可以在 Twitter 应用程序的照片视图或 Snapchat 的“发现”模式中找到这种方法。

类似的线程指出,当用户向下滑动时,我们可以使用 UISwipeGestureIdentiizer 和[ self disseViewControllerAnimated... ]来解除模态 VC。但是这只能处理一次滑动,不允许用户拖动模态。

89473 次浏览

You can use a UIPanGestureRecognizer to detect the user's drag and move the modal view with it. If the ending position is far enough down, the view can be dismissed, or otherwise animated back to its original position.

Check out this answer for more information on how to implement something like this.

What you're describing is an interactive custom transition animation. You are customizing both the animation and the driving gesture of a transition, i.e. the dismissal (or not) of a presented view controller. The easiest way to implement it is by combining a UIPanGestureRecognizer with a UIPercentDrivenInteractiveTransition.

My book explains how to do this, and I have posted examples (from the book). This particular example is a different situation - the transition is sideways, not down, and it is for a tab bar controller, not a presented controller - but the basic idea is exactly the same:

https://github.com/mattneub/Programming-iOS-Book-Examples/blob/master/bk2ch06p300customAnimation3/ch19p620customAnimation1/Animator.swift

If you download that project and run it, you will see that what is happening is exactly what you are describing, except that it is sideways: if the drag is more than half, we transition, but if not, we cancel and snap back into place.

I just created a tutorial for interactively dragging down a modal to dismiss it.

http://www.thorntech.com/2016/02/ios-tutorial-close-modal-dragging/

I found this topic to be confusing at first, so the tutorial builds this out step-by-step.

enter image description here

If you just want to run the code yourself, this is the repo:

https://github.com/ThornTechPublic/InteractiveModal

This is the approach I used:

View Controller

You override the dismiss animation with a custom one. If the user is dragging the modal, the interactor kicks in.

import UIKit


class ViewController: UIViewController {
let interactor = Interactor()
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let destinationViewController = segue.destinationViewController as? ModalViewController {
destinationViewController.transitioningDelegate = self
destinationViewController.interactor = interactor
}
}
}


extension ViewController: UIViewControllerTransitioningDelegate {
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
DismissAnimator()
}
func interactionControllerForDismissal(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
interactor.hasStarted ? interactor : .none
}
}

Dismiss Animator

You create a custom animator. This is a custom animation that you package inside a UIViewControllerAnimatedTransitioning protocol.

import UIKit


class DismissAnimator : NSObject {
let transitionDuration = 0.6
}


extension DismissAnimator : UIViewControllerAnimatedTransitioning {
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
transitionDuration
}
    

func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
guard
let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey),
let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey),
let containerView = transitionContext.containerView()
else {
return
}
if transitionContext.transitionWasCancelled {
containerView.insertSubview(toVC.view, belowSubview: fromVC.view)
}
let screenBounds = UIScreen.mainScreen().bounds
let bottomLeftCorner = CGPoint(x: 0, y: screenBounds.height)
let finalFrame = CGRect(origin: bottomLeftCorner, size: screenBounds.size)
        

UIView.animateWithDuration(
transitionDuration(transitionContext),
animations: {
fromVC.view.frame = finalFrame
},
completion: { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
}
)
}
}

Interactor

You subclass UIPercentDrivenInteractiveTransition so that it can act as your state machine. Since the interactor object is accessed by both VCs, use it to keep track of the panning progress.

import UIKit


class Interactor: UIPercentDrivenInteractiveTransition {
var hasStarted = false
var shouldFinish = false
}

Modal View Controller

This maps the pan gesture state to interactor method calls. The translationInView() y value determines whether the user crossed a threshold. When the pan gesture is .Ended, the interactor either finishes or cancels.

import UIKit


class ModalViewController: UIViewController {


var interactor:Interactor? = nil
    

@IBAction func close(sender: UIButton) {
dismiss(animated: true)
}


@IBAction func handleGesture(sender: UIPanGestureRecognizer) {
let percentThreshold:CGFloat = 0.3
        

let translation = sender.translation(in: view)
let verticalMovement = translation.y / view.bounds.height
let downwardMovement = fmaxf(Float(verticalMovement), 0.0)
let downwardMovementPercent = fminf(downwardMovement, 1.0)
let progress = CGFloat(downwardMovementPercent)
guard interactor = interactor else { return }


switch sender.state {
case .began:
interactor.hasStarted = true
dismiss(animated: true)
case .changed:
interactor.shouldFinish = progress > percentThreshold
interactor.update(progress)
case .cancelled:
interactor.hasStarted = false
interactor.cancel()
case .ended:
interactor.hasStarted = false
interactor.shouldFinish ? interactor.finish() :
interactor.cancel()
default:
break
}
}
    

}

created a demo for interactively dragging down to dismiss view controller like snapchat's discover mode. Check this github for sample project.

enter image description here

In Objective C : Here's the code

inviewDidLoad

UISwipeGestureRecognizer *swipeRecognizer = [[UISwipeGestureRecognizer alloc]
initWithTarget:self action:@selector(swipeDown:)];
swipeRecognizer.direction = UISwipeGestureRecognizerDirectionDown;
[self.view addGestureRecognizer:swipeRecognizer];


//Swipe Down Method


- (void)swipeDown:(UIGestureRecognizer *)sender{
[self dismissViewControllerAnimated:YES completion:nil];
}

I've created an easy to use extension.

Just inherent Your UIViewController with InteractiveViewController and you are done InteractiveViewController

call method showInteractive() from your controller to show as Interactive.

enter image description here

Massively updates the repo for Swift 4.

For Swift 3, I have created the following to present a UIViewController from right to left and dismiss it by pan gesture. I have uploaded this as a GitHub repository.

enter image description here

DismissOnPanGesture.swift file:

//  Created by David Seek on 11/21/16.
//  Copyright © 2016 David Seek. All rights reserved.


import UIKit


class DismissAnimator : NSObject {
}


extension DismissAnimator : UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.6
}


func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {


let screenBounds = UIScreen.main.bounds
let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)
let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
var x:CGFloat      = toVC!.view.bounds.origin.x - screenBounds.width
let y:CGFloat      = toVC!.view.bounds.origin.y
let width:CGFloat  = toVC!.view.bounds.width
let height:CGFloat = toVC!.view.bounds.height
var frame:CGRect   = CGRect(x: x, y: y, width: width, height: height)


toVC?.view.alpha = 0.2


toVC?.view.frame = frame
let containerView = transitionContext.containerView


containerView.insertSubview(toVC!.view, belowSubview: fromVC!.view)




let bottomLeftCorner = CGPoint(x: screenBounds.width, y: 0)
let finalFrame = CGRect(origin: bottomLeftCorner, size: screenBounds.size)


UIView.animate(
withDuration: transitionDuration(using: transitionContext),
animations: {
fromVC!.view.frame = finalFrame
toVC?.view.alpha = 1


x = toVC!.view.bounds.origin.x
frame = CGRect(x: x, y: y, width: width, height: height)


toVC?.view.frame = frame
},
completion: { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
)
}
}


class Interactor: UIPercentDrivenInteractiveTransition {
var hasStarted = false
var shouldFinish = false
}


let transition: CATransition = CATransition()


func presentVCRightToLeft(_ fromVC: UIViewController, _ toVC: UIViewController) {
transition.duration = 0.5
transition.type = kCATransitionPush
transition.subtype = kCATransitionFromRight
fromVC.view.window!.layer.add(transition, forKey: kCATransition)
fromVC.present(toVC, animated: false, completion: nil)
}


func dismissVCLeftToRight(_ vc: UIViewController) {
transition.duration = 0.5
transition.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
transition.type = kCATransitionPush
transition.subtype = kCATransitionFromLeft
vc.view.window!.layer.add(transition, forKey: nil)
vc.dismiss(animated: false, completion: nil)
}


func instantiatePanGestureRecognizer(_ vc: UIViewController, _ selector: Selector) {
var edgeRecognizer: UIScreenEdgePanGestureRecognizer!
edgeRecognizer = UIScreenEdgePanGestureRecognizer(target: vc, action: selector)
edgeRecognizer.edges = .left
vc.view.addGestureRecognizer(edgeRecognizer)
}


func dismissVCOnPanGesture(_ vc: UIViewController, _ sender: UIScreenEdgePanGestureRecognizer, _ interactor: Interactor) {
let percentThreshold:CGFloat = 0.3
let translation = sender.translation(in: vc.view)
let fingerMovement = translation.x / vc.view.bounds.width
let rightMovement = fmaxf(Float(fingerMovement), 0.0)
let rightMovementPercent = fminf(rightMovement, 1.0)
let progress = CGFloat(rightMovementPercent)


switch sender.state {
case .began:
interactor.hasStarted = true
vc.dismiss(animated: true, completion: nil)
case .changed:
interactor.shouldFinish = progress > percentThreshold
interactor.update(progress)
case .cancelled:
interactor.hasStarted = false
interactor.cancel()
case .ended:
interactor.hasStarted = false
interactor.shouldFinish
? interactor.finish()
: interactor.cancel()
default:
break
}
}

Easy usage:

import UIKit


class VC1: UIViewController, UIViewControllerTransitioningDelegate {


let interactor = Interactor()


@IBAction func present(_ sender: Any) {
let vc = self.storyboard?.instantiateViewController(withIdentifier: "VC2") as! VC2
vc.transitioningDelegate = self
vc.interactor = interactor


presentVCRightToLeft(self, vc)
}


func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return DismissAnimator()
}


func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return interactor.hasStarted ? interactor : nil
}
}


class VC2: UIViewController {


var interactor:Interactor? = nil


override func viewDidLoad() {
super.viewDidLoad()
instantiatePanGestureRecognizer(self, #selector(gesture))
}


@IBAction func dismiss(_ sender: Any) {
dismissVCLeftToRight(self)
}


func gesture(_ sender: UIScreenEdgePanGestureRecognizer) {
dismissVCOnPanGesture(self, sender, interactor!)
}
}

I'll share how I did it in Swift 3 :

Result

Implementation

class MainViewController: UIViewController {


@IBAction func click() {
performSegue(withIdentifier: "showModalOne", sender: nil)
}
  

}

class ModalOneViewController: ViewControllerPannable {
override func viewDidLoad() {
super.viewDidLoad()
    

view.backgroundColor = .yellow
}
  

@IBAction func click() {
performSegue(withIdentifier: "showModalTwo", sender: nil)
}
}

class ModalTwoViewController: ViewControllerPannable {
override func viewDidLoad() {
super.viewDidLoad()
    

view.backgroundColor = .green
}
}

Where the Modals View Controllers inherit from a class that I've built (ViewControllerPannable) to make them draggable and dismissible when reach certain velocity.

ViewControllerPannable class

class ViewControllerPannable: UIViewController {
var panGestureRecognizer: UIPanGestureRecognizer?
var originalPosition: CGPoint?
var currentPositionTouched: CGPoint?
  

override func viewDidLoad() {
super.viewDidLoad()
    

panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(panGestureAction(_:)))
view.addGestureRecognizer(panGestureRecognizer!)
}
  

@objc func panGestureAction(_ panGesture: UIPanGestureRecognizer) {
let translation = panGesture.translation(in: view)
    

if panGesture.state == .began {
originalPosition = view.center
currentPositionTouched = panGesture.location(in: view)
} else if panGesture.state == .changed {
view.frame.origin = CGPoint(
x: translation.x,
y: translation.y
)
} else if panGesture.state == .ended {
let velocity = panGesture.velocity(in: view)


if velocity.y >= 1500 {
UIView.animate(withDuration: 0.2
, animations: {
self.view.frame.origin = CGPoint(
x: self.view.frame.origin.x,
y: self.view.frame.size.height
)
}, completion: { (isCompleted) in
if isCompleted {
self.dismiss(animated: false, completion: nil)
}
})
} else {
UIView.animate(withDuration: 0.2, animations: {
self.view.center = self.originalPosition!
})
}
}
}
}

Only vertical dismiss

func panGestureAction(_ panGesture: UIPanGestureRecognizer) {
let translation = panGesture.translation(in: view)


if panGesture.state == .began {
originalPosition = view.center
currentPositionTouched = panGesture.location(in: view)
} else if panGesture.state == .changed {
view.frame.origin = CGPoint(
x:  view.frame.origin.x,
y:  view.frame.origin.y + translation.y
)
panGesture.setTranslation(CGPoint.zero, in: self.view)
} else if panGesture.state == .ended {
let velocity = panGesture.velocity(in: view)
if velocity.y >= 150 {
UIView.animate(withDuration: 0.2
, animations: {
self.view.frame.origin = CGPoint(
x: self.view.frame.origin.x,
y: self.view.frame.size.height
)
}, completion: { (isCompleted) in
if isCompleted {
self.dismiss(animated: false, completion: nil)
}
})
} else {
UIView.animate(withDuration: 0.2, animations: {
self.view.center = self.originalPosition!
})
}
}

Here is an extension I made based on @Wilson answer :

// MARK: IMPORT STATEMENTS
import UIKit


// MARK: EXTENSION
extension UIViewController {


// MARK: IS SWIPABLE - FUNCTION
func isSwipable() {
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
self.view.addGestureRecognizer(panGestureRecognizer)
}


// MARK: HANDLE PAN GESTURE - FUNCTION
@objc func handlePanGesture(_ panGesture: UIPanGestureRecognizer) {
let translation = panGesture.translation(in: view)
let minX = view.frame.width * 0.135
var originalPosition = CGPoint.zero


if panGesture.state == .began {
originalPosition = view.center
} else if panGesture.state == .changed {
view.frame.origin = CGPoint(x: translation.x, y: 0.0)


if panGesture.location(in: view).x > minX {
view.frame.origin = originalPosition
}


if view.frame.origin.x <= 0.0 {
view.frame.origin.x = 0.0
}
} else if panGesture.state == .ended {
if view.frame.origin.x >= view.frame.width * 0.5 {
UIView.animate(withDuration: 0.2
, animations: {
self.view.frame.origin = CGPoint(
x: self.view.frame.size.width,
y: self.view.frame.origin.y
)
}, completion: { (isCompleted) in
if isCompleted {
self.dismiss(animated: false, completion: nil)
}
})
} else {
UIView.animate(withDuration: 0.2, animations: {
self.view.frame.origin = originalPosition
})
}
}
}


}

USAGE

Inside your view controller you want to be swipable :

override func viewDidLoad() {
super.viewDidLoad()


self.isSwipable()
}

and it will be dismissible by swiping from the extreme left side of the view controller, as a navigation controller.

Here is a one-file solution based on @wilson's answer (thanks 👍 ) with the following improvements:


List of Improvements from previous solution

  • Limit panning so that the view only goes down:
    • Avoid horizontal translation by only updating the y coordinate of view.frame.origin
    • Avoid panning out of the screen when swiping up with let y = max(0, translation.y)
  • Also dismiss the view controller based on where the finger is released (defaults to the bottom half of the screen) and not just based on the velocity of the swipe
  • Show view controller as modal to ensure the previous viewcontroller appears behind and avoid a black background (should answer your question @nguyễn-anh-việt)
  • Remove unneeded currentPositionTouched and originalPosition
  • Expose the following parameters:
    • minimumVelocityToHide: what speed is enough to hide (defaults to 1500)
    • minimumScreenRatioToHide: how low is enough to hide (defaults to 0.5)
    • animationDuration : how fast do we hide/show (defaults to 0.2s)

Solution

Swift 3 & Swift 4 :

//
//  PannableViewController.swift
//


import UIKit


class PannableViewController: UIViewController {
public var minimumVelocityToHide: CGFloat = 1500
public var minimumScreenRatioToHide: CGFloat = 0.5
public var animationDuration: TimeInterval = 0.2


override func viewDidLoad() {
super.viewDidLoad()


// Listen for pan gesture
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(onPan(_:)))
view.addGestureRecognizer(panGesture)
}


@objc func onPan(_ panGesture: UIPanGestureRecognizer) {


func slideViewVerticallyTo(_ y: CGFloat) {
self.view.frame.origin = CGPoint(x: 0, y: y)
}


switch panGesture.state {


case .began, .changed:
// If pan started or is ongoing then
// slide the view to follow the finger
let translation = panGesture.translation(in: view)
let y = max(0, translation.y)
slideViewVerticallyTo(y)


case .ended:
// If pan ended, decide it we should close or reset the view
// based on the final position and the speed of the gesture
let translation = panGesture.translation(in: view)
let velocity = panGesture.velocity(in: view)
let closing = (translation.y > self.view.frame.size.height * minimumScreenRatioToHide) ||
(velocity.y > minimumVelocityToHide)


if closing {
UIView.animate(withDuration: animationDuration, animations: {
// If closing, animate to the bottom of the view
self.slideViewVerticallyTo(self.view.frame.size.height)
}, completion: { (isCompleted) in
if isCompleted {
// Dismiss the view when it dissapeared
dismiss(animated: false, completion: nil)
}
})
} else {
// If not closing, reset the view to the top
UIView.animate(withDuration: animationDuration, animations: {
slideViewVerticallyTo(0)
})
}


default:
// If gesture state is undefined, reset the view to the top
UIView.animate(withDuration: animationDuration, animations: {
slideViewVerticallyTo(0)
})


}
}


override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?)   {
super.init(nibName: nil, bundle: nil)
modalPresentationStyle = .overFullScreen;
modalTransitionStyle = .coverVertical;
}


required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
modalPresentationStyle = .overFullScreen;
modalTransitionStyle = .coverVertical;
}
}

Swift 4.x, Using Pangesture

Simple way

Horizontal

class ViewConrtoller: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(onDrage(_:))))
}


@objc func onDrage(_ sender:UIPanGestureRecognizer) {
let percentThreshold:CGFloat = 0.3
let translation = sender.translation(in: view)
    

let newX = ensureRange(value: view.frame.minX + translation.x, minimum: 0, maximum: view.frame.maxX)
let progress = progressAlongAxis(newX, view.bounds.width)
    

view.frame.origin.x = newX //Move view to new position
    

if sender.state == .ended {
let velocity = sender.velocity(in: view)
if velocity.x >= 300 || progress > percentThreshold {
self.dismiss(animated: true) //Perform dismiss
} else {
UIView.animate(withDuration: 0.2, animations: {
self.view.frame.origin.x = 0 // Revert animation
})
}
}
    

sender.setTranslation(.zero, in: view)
}
}

Helper function

func progressAlongAxis(_ pointOnAxis: CGFloat, _ axisLength: CGFloat) -> CGFloat {
let movementOnAxis = pointOnAxis / axisLength
let positiveMovementOnAxis = fmaxf(Float(movementOnAxis), 0.0)
let positiveMovementOnAxisPercent = fminf(positiveMovementOnAxis, 1.0)
return CGFloat(positiveMovementOnAxisPercent)
}
    

func ensureRange<T>(value: T, minimum: T, maximum: T) -> T where T : Comparable {
return min(max(value, minimum), maximum)
}

#Hard way

Refer this -> https://github.com/satishVekariya/DraggableViewController

I figured out super simple way to do this. Just put the following code into your view controller:

Swift 4

override func viewDidLoad() {
super.viewDidLoad()
let gestureRecognizer = UIPanGestureRecognizer(target: self,
action: #selector(panGestureRecognizerHandler(_:)))
view.addGestureRecognizer(gestureRecognizer)
}


@IBAction func panGestureRecognizerHandler(_ sender: UIPanGestureRecognizer) {
let touchPoint = sender.location(in: view?.window)
var initialTouchPoint = CGPoint.zero


switch sender.state {
case .began:
initialTouchPoint = touchPoint
case .changed:
if touchPoint.y > initialTouchPoint.y {
view.frame.origin.y = touchPoint.y - initialTouchPoint.y
}
case .ended, .cancelled:
if touchPoint.y - initialTouchPoint.y > 200 {
dismiss(animated: true, completion: nil)
} else {
UIView.animate(withDuration: 0.2, animations: {
self.view.frame = CGRect(x: 0,
y: 0,
width: self.view.frame.size.width,
height: self.view.frame.size.height)
})
}
case .failed, .possible:
break
}
}

This my simple class for Drag ViewController from axis. Just herited your class from DraggableViewController.

MyCustomClass: DraggableViewController

Work only for presented ViewController.

// MARK: - DraggableViewController


public class DraggableViewController: UIViewController {


public let percentThresholdDismiss: CGFloat = 0.3
public var velocityDismiss: CGFloat = 300
public var axis: NSLayoutConstraint.Axis = .horizontal
public var backgroundDismissColor: UIColor = .black {
didSet {
navigationController?.view.backgroundColor = backgroundDismissColor
}
}


// MARK: LifeCycle


override func viewDidLoad() {
super.viewDidLoad()
view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(onDrag(_:))))
}


// MARK: Private methods


@objc fileprivate func onDrag(_ sender: UIPanGestureRecognizer) {


let translation = sender.translation(in: view)


// Movement indication index
let movementOnAxis: CGFloat


// Move view to new position
switch axis {
case .vertical:
let newY = min(max(view.frame.minY + translation.y, 0), view.frame.maxY)
movementOnAxis = newY / view.bounds.height
view.frame.origin.y = newY


case .horizontal:
let newX = min(max(view.frame.minX + translation.x, 0), view.frame.maxX)
movementOnAxis = newX / view.bounds.width
view.frame.origin.x = newX
}


let positiveMovementOnAxis = fmaxf(Float(movementOnAxis), 0.0)
let positiveMovementOnAxisPercent = fminf(positiveMovementOnAxis, 1.0)
let progress = CGFloat(positiveMovementOnAxisPercent)
navigationController?.view.backgroundColor = UIColor.black.withAlphaComponent(1 - progress)


switch sender.state {
case .ended where sender.velocity(in: view).y >= velocityDismiss || progress > percentThresholdDismiss:
// After animate, user made the conditions to leave
UIView.animate(withDuration: 0.2, animations: {
switch self.axis {
case .vertical:
self.view.frame.origin.y = self.view.bounds.height


case .horizontal:
self.view.frame.origin.x = self.view.bounds.width
}
self.navigationController?.view.backgroundColor = UIColor.black.withAlphaComponent(0)


}, completion: { finish in
self.dismiss(animated: true) //Perform dismiss
})
case .ended:
// Revert animation
UIView.animate(withDuration: 0.2, animations: {
switch self.axis {
case .vertical:
self.view.frame.origin.y = 0


case .horizontal:
self.view.frame.origin.x = 0
}
})
default:
break
}
sender.setTranslation(.zero, in: view)
}
}

For those who really wanna dive a little deeper into Custom UIViewController Transition, I recommend this great tutorial from raywenderlich.com.

The original final sample project contains bug. So I fixed it and upload it to Github repo. The proj is in Swift 5, so you can easily run and play it.

Here is a preview:

And it's interactive too!

Happy hacking!

For Swift 4 + Swift 5, using UIPanGestureRecognizer. Based on @SPatel 's answer above.

Add these two helper functions:

func progressAlongAxis(_ pointOnAxis: CGFloat, _ axisLength: CGFloat) -> CGFloat {
let movementOnAxis = pointOnAxis / axisLength
let positiveMovementOnAxis = fmaxf(Float(movementOnAxis), 0.0)
let positiveMovementOnAxisPercent = fminf(positiveMovementOnAxis, 1.0)
return CGFloat(positiveMovementOnAxisPercent)
}


func ensureRange<T>(value: T, minimum: T, maximum: T) -> T where T: Comparable {
return min(max(value, minimum), maximum)
}

To dismiss by dragging down:


class SwipeDownViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()


// dismiss dragging vertically:
view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(onDragY(_:))))
}


@objc func onDragY(_ sender: UIPanGestureRecognizer) {
let percentThreshold: CGFloat = 0.3
let translation = sender.translation(in: view)


let newY = ensureRange(value: view.frame.minY + translation.y, minimum: 0, maximum: view.frame.maxY)
let progress = progressAlongAxis(newY, view.bounds.height)


view.frame.origin.y = newY // Move view to new position


if sender.state == .ended {
let velocity = sender.velocity(in: view)
if velocity.y >= 300 || progress > percentThreshold {
dismiss(animated: true) // Perform dismiss
} else {
UIView.animate(withDuration: 0.2, animations: {
self.view.frame.origin.y = 0 // Revert animation
})
}
}


sender.setTranslation(.zero, in: view)
}
}


To dismiss by dragging right:


class SwipeRightViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()


// dismiss dragging horizontally:
view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(onDragX(_:))))
}


@objc func onDragX(_ sender: UIPanGestureRecognizer) {
let percentThreshold: CGFloat = 0.3
let translation = sender.translation(in: view)


let newX = ensureRange(value: view.frame.minX + translation.x, minimum: 0, maximum: view.frame.maxX)
let progress = progressAlongAxis(newX, view.bounds.width)


view.frame.origin.x = newX // Move view to new position


if sender.state == .ended {
let velocity = sender.velocity(in: view)
if velocity.x >= 300 || progress > percentThreshold {
dismiss(animated: true) // Perform dismiss
} else {
UIView.animate(withDuration: 0.2, animations: {
self.view.frame.origin.x = 0 // Revert animation
})
}
}


sender.setTranslation(.zero, in: view)
}
}