How to disable a gesture recognizer while editing a text field? - ios

I have a UITextView that has a pan gesture recognizer so it is draggable around the screen. I am trying to disable this gesture recognizer while the text field is being edited so the text stays in the middle of the screen, and the user can't drag it below the keyboard.
I have tried answers like this and this but can't seem to get them to work. I've also tried removing the gesture recognizer using textField.removeGestureRecognizer(textField.panGestureRecognizer)
but couldn't get that to work either. Does anyone know what is going wrong here?
Here is my code;
import UIKit
class ViewController: UIViewController, UIGestureRecognizerDelegate {
let textField = UITextView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 100))
//the current center of the text being edited
var currentCenter = CGPoint()
override func viewDidLoad() {
super.viewDidLoad()
// tap gesture recognizer for keyboard hide
let HideKeyboardTap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
view.addGestureRecognizer(HideKeyboardTap)
textField.text = "hello"
textField.font = UIFont.systemFont(ofSize: 80)
textField.textContainerInset = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
textField.sizeToFit()
self.view.addSubview(textField)
//Enable multiple touch and user interaction for textfield
textField.isUserInteractionEnabled = true
textField.isMultipleTouchEnabled = true
//add pan gesture
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
panGestureRecognizer.delegate = self
textField.addGestureRecognizer(panGestureRecognizer)
NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardShowNotification), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardHideNotification), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
}
#objc func dismissKeyboard() {
view.endEditing(true)
}
#objc func handleKeyboardShowNotification(notification: NSNotification) {
if let keyboardFrame: NSValue = notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue {
let keyboardRectangle = keyboardFrame.cgRectValue
let keyboardHeight = keyboardRectangle.height
currentCenter = textField.center
textField.center = CGPoint(x: UIScreen.main.bounds.midX , y: UIScreen.main.bounds.height - (keyboardHeight + textField.bounds.height + 10))
textField.panGestureRecognizer.isEnabled = false
}
}
#objc func handleKeyboardHideNotification() {
textField.panGestureRecognizer.isEnabled = true
textField.center = currentCenter
}
#objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
if gestureRecognizer.state == .began || gestureRecognizer.state == .changed {
let translation = gestureRecognizer.translation(in: self.view)
gestureRecognizer.view!.center = CGPoint(x: gestureRecognizer.view!.center.x + translation.x, y: gestureRecognizer.view!.center.y + translation.y)
gestureRecognizer.setTranslation(CGPoint.zero, in: self.view)
}
}
}
Thanks for any help!

Instead of trying to disable the gesture, how about do nothing in the gesture action while editing?
#objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
if(textField.isEditing){return;}
if gestureRecognizer.state == .began || gestureRecognizer.state == .changed {
let translation = gestureRecognizer.translation(in: self.view)
gestureRecognizer.view!.center = CGPoint(x: gestureRecognizer.view!.center.x + translation.x, y: gestureRecognizer.view!.center.y + translation.y)
gestureRecognizer.setTranslation(CGPoint.zero, in: self.view)
}
}

private func getPanGesture() -> [UIPanGestureRecognizer]? {
let panGesture = textField.gestureRecognizers?.filter({$0 is UIPanGestureRecognizer})
return panGesture as? [UIPanGestureRecognizer]
}
private func disablePanGesture() {
let enabledGestures = getPanGesture()?.filter({$0.isEnabled})
enabledGestures?.forEach({ (gesture) in
gesture.isEnabled = false
})
}
private func enablePanGesture() {
let disabledGestures = getPanGesture()?.filter({!$0.isEnabled})
disabledGestures?.forEach({ (gesture) in
gesture.isEnabled = true
})
}
#objc func handleKeyboardShowNotification(notification: NSNotification) {
if let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
..
disablePanGesture()
}
}
#objc func handleKeyboardHideNotification() {
enablePanGesture()
..
}

Related

Zoom ImageView in UICollectionView inconsistent

I have a UICollectionView with cells that are the size of the CollectionView itself, so paging is enabled and you swipe through similar to the basic Gallery app.
I want the images to be zoomable like on Instagram (snaps back on gesture end). I Implemented the Pinch and Pan gestures with the help of an article, and it does work.
But I am having a strange issue. The speed at which it zooms is very inconsistent, and it seems to get faster with every page I flip. So the first Page it zooms and pans pretty normally, the second it is much faster, and by the third it is ridiculous.
I'm very confused, since I had this exact code running on a paging ScrollView, and it worked flawlessly.
Here is the relevant code in the UICollectionViewCell.
func setup(){
imageView.isUserInteractionEnabled = true
let pinch = UIPinchGestureRecognizer(target: self, action:
#selector(pinch(sender:)))
pinch.delegate = self
imageView.addGestureRecognizer(pinch)
let pan = UIPanGestureRecognizer(target: self, action:
#selector(pan(sender:)))
pan.delegate = self
imageView.addGestureRecognizer(pan)
let tap = UITapGestureRecognizer(target: self, action:
#selector(handleTap))
imageView.addGestureRecognizer(tap)
imageView.clipsToBounds = false
}
#objc func pan(sender: UIPanGestureRecognizer) {
if(isZooming){
if self.isZooming && sender.state == .began {
self.originalImageCenter = sender.view?.center
} else if self.isZooming && sender.state == .changed {
let translation = sender.translation(in: self)
if let view = sender.view {
view.center = CGPoint(x:view.center.x +
translation.x,
y:view.center.y +
translation.y)
}
sender.setTranslation(CGPoint.zero, in:
self.imageView.superview)
}
}else{
print("blocked pan")
}
}
#objc func pinch(sender:UIPinchGestureRecognizer) {
NotificationCenter.default.post(name:
Notification.Name(rawValue: "disablePage"), object: nil)
allowedToTap = false
if sender.state == .began {
let currentScale = self.imageView.frame.size.width /
self.imageView.bounds.size.width
let newScale = currentScale*sender.scale
if newScale > 1 {
self.isZooming = true
}
} else if sender.state == .changed {
guard let view = sender.view else {return}
let pinchCenter = CGPoint(x: sender.location(in: view).x -
view.bounds.midX,
y: sender.location(in: view).y -
view.bounds.midY)
let transform = view.transform.translatedBy(x:
pinchCenter.x, y: pinchCenter.y)
.scaledBy(x: sender.scale, y: sender.scale)
.translatedBy(x: -pinchCenter.x, y: -pinchCenter.y)
let currentScale = self.imageView.frame.size.width /
self.imageView.bounds.size.width
var newScale = currentScale*sender.scale
if newScale < 1 {
newScale = 1
let transform = CGAffineTransform(scaleX: newScale, y:
newScale)
self.imageView.transform = transform
sender.scale = 1
}else {
view.transform = transform
sender.scale = 1
}
} else if sender.state == .ended || sender.state == .failed ||
sender.state == .cancelled {
guard let center = self.originalImageCenter else {return}
UIView.animate(withDuration: 0.3, animations: {
self.imageView.transform = CGAffineTransform.identity
self.imageView.center = center
}, completion: { _ in
self.isZooming = false
NotificationCenter.default.post(name:
Notification.Name(rawValue: "enablePage"), object: nil)
self.allowedToTap = true
})
}
}
I hope someone with more of an idea of how this works can help me.
I found a solution, I ll leave it here in case someone has the same issue.
The problem was that the setup function was being called multiple times, therefore multiple identical GestureRecognizers were being added.
So I just added a quick check.
func setup(){
imageView.isUserInteractionEnabled = true
playPauseImageView.alpha = 0
if(!gesturesAreAttached){
gesturesAreAttached = true
addZoom()
}
imageView.clipsToBounds = false
}
func addZoom(){
print("adding Zoom")
let pinch = UIPinchGestureRecognizer(target: self, action: #selector(pinch(sender:)))
pinch.delegate = self
imageView.addGestureRecognizer(pinch)
let pan = UIPanGestureRecognizer(target: self, action: #selector(pan(sender:)))
pan.delegate = self
imageView.addGestureRecognizer(pan)
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap))
imageView.addGestureRecognizer(tap)
}

How to create swipe to start button with moving arrows

I want to create the exactly the same swipe button like this https://github.com/shadowfaxtech/proSwipeButton .
I was wondering how to change the arrow of the button on user touches
I was doing this for getting swipe action.
let rightSwipe = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipes(_:)))
rightSwipe.direction = .right
view.addGestureRecognizer(rightSwipe)
but the thing is how to add arrows to button which change there position on user touches.
Here is the code I have written for swiping over the button. You assign image to the image view.
func createSwipeButton() {
let button = UIButton.init(type: .custom)
button.backgroundColor = UIColor.brown
button.setTitle("PLACE ORDER", for: .normal)
button.frame = CGRect.init(x: 10, y: 200, width: self.view.frame.size.width-20, height: 100)
button.addTarget(self, action: #selector(swiped(_:event:)), for: .touchDragInside)
button.addTarget(self, action: #selector(swipeEnded(_:event:)), for: .touchUpInside)
self.view.addSubview(button)
let swipableView = UIImageView.init()
swipableView.frame = CGRect.init(x: 0, y: 0, width: 20, height: button.frame.size.height)
swipableView.tag = 20
swipableView.backgroundColor = UIColor.white
button.addSubview(swipableView)
}
#objc func swiped(_ sender : UIButton, event: UIEvent) {
let swipableView = sender.viewWithTag(20)!
let centerPosition = location(event: event, subView: swipableView, superView: sender,isSwiping: true)
UIView.animate(withDuration: 0.2) {
swipableView.center = centerPosition
}
}
#objc func swipeEnded(_ sender : UIButton, event: UIEvent) {
let swipableView = sender.viewWithTag(20)!
let centerPosition = location(event: event, subView: swipableView, superView: sender, isSwiping: false)
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 5, options: .curveEaseInOut, animations: {
swipableView.center = centerPosition
}) { _ in}
}
func location(event: UIEvent, subView: UIView, superView: UIButton, isSwiping: Bool) -> CGPoint {
if let touch = event.touches(for: superView)?.first{
let previousLocation = touch.previousLocation(in: superView)
let location = touch.location(in: superView)
let delta_x = location.x - previousLocation.x;
print(subView.center.x + delta_x)
var centerPosition = CGPoint.init(x: subView.center.x + delta_x, y: subView.center.y)
let minX = subView.frame.size.width/2
let maxX = superView.frame.size.width - subView.frame.size.width/2
centerPosition.x = centerPosition.x < minX ? minX : centerPosition.x
centerPosition.x = centerPosition.x > maxX ? maxX : centerPosition.x
if !isSwiping{
let normalPosition = superView.frame.size.width * 0.5
centerPosition.x = centerPosition.x > normalPosition ? maxX : minX
centerPosition.x = centerPosition.x <= normalPosition ? minX : centerPosition.x
}
return centerPosition
}
return CGPoint.zero
}
Complete project is on github: https://github.com/IamSaurav/SwipeButton
Mmm what about something like this?
You can add an UIImage in the storyboard in the swipeImage var.
The best effect is done if the image has the same color of the text.
import UIKit
#IBDesignable
class UISwipeableLabel: UILabel {
#IBInspectable var swipeImage: UIImage? {
didSet {
configureSwipeImage()
}
}
private var swipeImageView: UIImageView?
private var rightSwipe: UIPanGestureRecognizer?
private var shouldActivateButton = true
override func awakeFromNib() {
super.awakeFromNib()
configureSwipeImage()
clipsToBounds = true
}
}
private extension UISwipeableLabel {
#objc func handleSwipes(_ sender:UIPanGestureRecognizer) {
if let centerX = swipeImageView?.center.x {
let translation = sender.translation(in: self)
let percent = centerX/frame.width
if sender.state == .changed {
if centerX < frame.width - frame.height/2 {
swipeImageView?.center.x = centerX + translation.x
sender.setTranslation(CGPoint.zero, in: swipeImageView)
} else {
swipeImageView?.center.x = frame.width - frame.height/2
if shouldActivateButton {
activateButton()
}
}
}
if sender.state == .ended || sender.state == .cancelled || sender.state == .failed {
if shouldActivateButton {
UIView.animate(withDuration: 0.25 * TimeInterval(percent)) {
self.swipeImageView?.center.x = self.frame.height/2
}
}
}
}
}
func configureSwipeImage() {
if swipeImageView != nil {
swipeImageView?.removeFromSuperview()
}
swipeImageView = UIImageView(frame: CGRect(x: 0, y: 0, width: frame.height, height: frame.height))
if let swipeImageView = swipeImageView {
swipeImageView.image = swipeImage
swipeImageView.isUserInteractionEnabled = true
swipeImageView.alpha = 0.5
addSubview(swipeImageView)
rightSwipe = UIPanGestureRecognizer(target: self, action: #selector(handleSwipes(_:)))
if let rightSwipe = rightSwipe {
swipeImageView.addGestureRecognizer(rightSwipe)
}
}
}
func activateButton() {
print("*** DO YOUR STUFF HERE ***")
}
}
You start with a UILabel and if you want, change it to use autolayout.

How can I set UIGestureRecognizer's Speed as the velocity for my Physicsbody in SpriteKit?

I calculated the speed of UIGestureRecognizer and I want to use it as velocity of Physicsbody in SpriteKit.
How can I do that since Speed is calculated in touches began and touches ended method, while my Physicsbody is wrapped in swipeUp & swipeDown etc functions, which does not have access to the Speed variable?
Here is my code:
class GameScene: SKScene, SKPhysicsContactDelegate, SKViewDelegate, UIGestureRecognizerDelegate, UIViewControllerTransitioningDelegate {
var Kite = SKSpriteNode(imageNamed: "ASC_8025_large.jpg")
#objc let minusButton = SKSpriteNode(imageNamed: "minus.jpg")
#objc let plusButton = SKSpriteNode(imageNamed: "plus.png")
//override init(Kite: SKSpriteNode) {
//For long Long press gesture to work
let longPressGestureRecPlus = UILongPressGestureRecognizer()
let longPressGestureRecMinus = UILongPressGestureRecognizer(target: self, action: #selector(longPressed(press:)))
//First we declare all of our Gestures...
//swipes
let swipeRightRec = UISwipeGestureRecognizer()
let swipeLeftRec = UISwipeGestureRecognizer()
let swipeUpRec = UISwipeGestureRecognizer()
let swipeDownRec = UISwipeGestureRecognizer()
//rotate
let rotateRec = UIRotationGestureRecognizer()
//taps
let tapRec = UITapGestureRecognizer()
let tapRec2 = UITapGestureRecognizer()
override func didMove(to view: SKView) {
// Get label node from scene and store it for use later
Kite.size = CGSize(width: 40.0, height: 40.0)
Kite.position = CGPoint(x: frame.midX, y: frame.midY)
plusButton.size = CGSize(width: 30.0, height: 30.0)
plusButton.position = CGPoint(x: frame.midX, y: frame.minY + 20.0)
plusButton.name = "plusButton"
//plusButton.isUserInteractionEnabled = true
addChild(Kite)
addChild(plusButton)
swipeRightRec.addTarget(self, action: #selector(GameScene.swipedRight) )
swipeRightRec.direction = .right
self.view!.addGestureRecognizer(swipeRightRec)
swipeLeftRec.addTarget(self, action: #selector(GameScene.swipedLeft) )
swipeLeftRec.direction = .left
self.view!.addGestureRecognizer(swipeLeftRec)
swipeUpRec.addTarget(self, action: #selector(GameScene.swipedUp) )
swipeUpRec.direction = .up
self.view!.addGestureRecognizer(swipeUpRec)
swipeDownRec.addTarget(self, action: #selector(GameScene.swipedDown) )
swipeDownRec.direction = .down
self.view!.addGestureRecognizer(swipeDownRec)
//notice the function this calls has (_:) after it because we are passing in info about the gesture itself (the sender)
rotateRec.addTarget(self, action: #selector (GameScene.rotatedView (_:) ))
self.view!.addGestureRecognizer(rotateRec)
// again notice (_:), we'll need this to find out where the tap occurred.
tapRec.addTarget(self, action:#selector(GameScene.tappedView(_:) ))
tapRec.numberOfTouchesRequired = 1
tapRec.numberOfTapsRequired = 1
self.view!.addGestureRecognizer(tapRec)
tapRec2.addTarget(self, action:#selector(GameScene.tappedView2(_:) ))
tapRec2.numberOfTouchesRequired = 1
tapRec2.numberOfTapsRequired = 2 //2 taps instead of 1 this time
self.view!.addGestureRecognizer(tapRec2)
longPressGestureRecPlus.addTarget(self, action: #selector(longPressed(press:)))
longPressGestureRecPlus.minimumPressDuration = 2.0
self.view?.addGestureRecognizer(longPressGestureRecPlus)
}
//the functions that get called when swiping...
#objc func swipedRight() {
print("Right")
Kite.physicsBody?.velocity = CGVector(dx: 60, dy: 60)
//Tilts the Kite towards Right
let tiltRight = SKAction.rotate(toAngle: -1.00, duration: 0.1)
Kite.run(tiltRight)
}
#objc func swipedLeft() {
Kite.physicsBody?.velocity = CGVector(dx: -60, dy: 60)
//Tilts the Kite towards Right
let tiltLeft = SKAction.rotate(toAngle: 1.00, duration: 0.1)
Kite.run(tiltLeft)
print("Left")
}
#objc func swipedUp() {
Kite.physicsBody?.velocity = CGVector(dx: 60, dy: 60)
//Straightens the Kite
let straightens = SKAction.rotate(toAngle: 0.00, duration: 0.1)
Kite.run(straightens)
print("Up")
}
#objc func swipedDown() {
Kite.physicsBody?.velocity = CGVector(dx: 0, dy: -60)
print("Down")
}
// what gets called when there's a single tap...
//notice the sender is a parameter. This is why we added (_:) that part to the selector earlier
#objc func tappedView(_ sender:UITapGestureRecognizer) {
let point:CGPoint = sender.location(in: self.view)
print("Single tap")
print(point)
}
// what gets called when there's a double tap...
//notice the sender is a parameter. This is why we added (_:) that part to the selector earlier
#objc func tappedView2(_ sender:UITapGestureRecognizer) {
let point:CGPoint = sender.location(in: self.view)
print("Double tap")
print(point)
}
//what gets called when there's a rotation gesture
//notice the sender is a parameter. This is why we added (_:) that part to the selector earlier
#objc func rotatedView(_ sender:UIRotationGestureRecognizer) {
if (sender.state == .began) {
print("rotation began")
}
if (sender.state == .changed) {
print("rotation changed")
//you could easily make any sprite's rotation equal this amount like so...
//thePlayer.zRotation = -sender.rotation
//convert rotation to degrees...
let rotateAmount = Measurement(value: Double(sender.rotation), unit: UnitAngle.radians).converted(to: .degrees).value
print("\(rotateAmount) degreess" )
}
if (sender.state == .ended) {
print("rotation ended")
}
}
func removeAllGestures(){
//if you need to remove all gesture recognizers with Swift you can do this....
for gesture in (self.view?.gestureRecognizers)! {
self.view?.removeGestureRecognizer(gesture)
}
//this is good to do before a SKScene transitions to another SKScene.
}
func removeAGesture()
{
//To remove a single gesture you can use...
self.view?.removeGestureRecognizer(swipeUpRec)
}
//Fix the gesture recognizing the plusButton
#objc func longPressed(press: UILongPressGestureRecognizer) {
if press.state == .began {
isUserInteractionEnabled = true
let positionInScene = press.location(in: self.view)
let touchedNode = self.atPoint(positionInScene)
plusButton.name = "plusButton"
Kite.physicsBody?.velocity = CGVector(dx: 0, dy: 60.0*3)
print("Pressed on the screen")
if let name = touchedNode.name {
if name == "plusButton" {
print("LONG TAPPED")
}
}
}
}
var start: CGPoint?
var startTime: TimeInterval?
var taps = 0
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
print("Began")
Kite.physicsBody = SKPhysicsBody(rectangleOf: Kite.size)
Kite.physicsBody?.affectedByGravity = false
//Kite.physicsBody?.velocity = CGVector(dx: 0, dy: 0)
Kite.physicsBody?.applyImpulse(CGVector(dx: 0, dy: 5))
plusButton.name = "plusButton"
plusButton.isUserInteractionEnabled = false
//We figure how to interact with BUTTON in Spritekit
let touch = touches.first
let positionInScene = touch!.location(in: self)
let touchedNode = self.atPoint(positionInScene)
if let name = touchedNode.name {
if name == "plusButton" {
taps += 1
Kite.size = CGSize(width: 200, height: 200)
print("tapped")
print("Taps", taps)
}
}
for touch in touches {
let location:CGPoint = touch.location(in: self.view!)
start = location
startTime = touch.timestamp
}
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
print("Ended")
//Calculating Speed of the Gestures
for touch in touches {
let location:CGPoint = touch.location(in: self.view!)
var dx:CGFloat = location.x - start!.x;
var dy:CGFloat = location.y - start!.y;
var magnitude:CGFloat = sqrt(dx*dx+dy*dy)
//Calculate Time
var dt:CGFloat = CGFloat(touch.timestamp - startTime!)
//Speed = Distance / Time
var speed:CGFloat = magnitude / dt
var speedX:CGFloat = dx/dt
var speedY:CGFloat = dy/dt
print("SpeedY", speedX)
print("SpeedY", speedY)
}
}
so calculating the speed from a vector like dx and dy is
let speed = sqrtf(Float(pow(dx!, 2) + pow(dy!, 2)))
So if you want apply the speed as velocity to an object you need at least one of the directions.
let newDx = 0 // means in no x direction
// next you can change the formula to your new dy to match the given speed
let newDy = sqrtf(pow(speed, 2) - Float(pow(newDx, 2)))
yourNode.physicsBody?.velocity = CGVector(dx: newDx, dy: CGFloat(newDy))

Swift: move UIView on slide gesture

I am trying to move a UIView on slide up gesture from its initial position to a fixed final position. The image should move with the hand gesture, and not animate independently.
I haven't tried anything as I have no clue where to start, which gesture class to use.
Finally did it like below.
let gesture = UIPanGestureRecognizer(target: self, action: Selector("wasDragged:"))
slideUpView.addGestureRecognizer(gesture)
slideUpView.userInteractionEnabled = true
gesture.delegate = self
The following function is called when the gesture is detected, (here I am restricting the view to have a maximum centre.y of 555, & I'm resetting back to 554 when the view moves past this point)
func wasDragged(gestureRecognizer: UIPanGestureRecognizer) {
if gestureRecognizer.state == UIGestureRecognizerState.Began || gestureRecognizer.state == UIGestureRecognizerState.Changed {
let translation = gestureRecognizer.translationInView(self.view)
print(gestureRecognizer.view!.center.y)
if(gestureRecognizer.view!.center.y < 555) {
gestureRecognizer.view!.center = CGPointMake(gestureRecognizer.view!.center.x, gestureRecognizer.view!.center.y + translation.y)
}else {
gestureRecognizer.view!.center = CGPointMake(gestureRecognizer.view!.center.x, 554)
}
gestureRecognizer.setTranslation(CGPointMake(0,0), inView: self.view)
}
}
You probably want to use a UIPanGestureRecognizer.
let gesture = UIPanGestureRecognizer(target: self, action: Selector("wasDragged:"))
customView.addGestureRecognizer(gesture)
gesture.delegate = self
And to drag the object only along the y-axis:
func wasDragged(gesture: UIPanGestureRecognizer) {
let translation = gesture.translationInView(self.view)
// Use translation.y to change the position of your customView, e.g.
customView.center.y = translation.y // Customize this.
}
Swift 4:
#objc func wasDragged(_ gestureRecognizer: UIPanGestureRecognizer) {
if gestureRecognizer.state == UIGestureRecognizer.State.began || gestureRecognizer.state == UIGestureRecognizer.State.changed {
let translation = gestureRecognizer.translation(in: self.view)
print(gestureRecognizer.view!.center.y)
if(gestureRecognizer.view!.center.y < 555) {
gestureRecognizer.view!.center = CGPoint(x: gestureRecognizer.view!.center.x, y: gestureRecognizer.view!.center.y + translation.y)
}else {
gestureRecognizer.view!.center = CGPoint(x:gestureRecognizer.view!.center.x, y:554)
}
gestureRecognizer.setTranslation(CGPoint(x: 0, y: 0), in: self.view)
}
}
Call
let gesture = UIPanGestureRecognizer(target: self, action: self.wasDragged(gestureRecognizer:))
customView.addGestureRecognizer(gesture)
gesture.delegate = self
Update for Swift 3.x
When assigning the selector, the syntax has changed and now requires #selector
let gesture = UIPanGestureRecognizer(target: self, action: #selector(navViewDragged(gesture:)))
self.navLayoutView.addGestureRecognizer(gesture)
self.navLayoutView.isUserInteractionEnabled = true
gesture.delegate = self
Function implementation:
func navViewDragged(gesture: UIPanGestureRecognizer){
//Code here
}
Move view anywhere in Swift 3
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(dragged(gestureRecognizer:)))
demoView.isUserInteractionEnabled = true
demoView.addGestureRecognizer(panGesture)
Function
#objc func dragged(gestureRecognizer: UIPanGestureRecognizer) {
if gestureRecognizer.state == UIGestureRecognizerState.began || gestureRecognizer.state == UIGestureRecognizerState.changed {
let translation = gestureRecognizer.translation(in: self.view)
gestureRecognizer.view!.center = CGPoint(x: gestureRecognizer.view!.center.x + translation.x, y: gestureRecognizer.view!.center.y + translation.y)
gestureRecognizer.setTranslation(CGPoint(x: 0, y: 0), in: self.view)
}
}
This is how you really do it like the news view in the Stocks app
First add 2 constraints in Storyboard to the sliding view, one for it's state when it's fully opened and one for when it's closed. Don't forget to leave one of the constraints disabled / not installed so that your view will look opened or closed when the scene is reached.
Reference them in your code
#IBOutlet weak var optionsOpenedConstraint: NSLayoutConstraint!
#IBOutlet weak var optionsVisiableConstraint: NSLayoutConstraint!
now add the UIPanGestureRecognizer to your view in the viewDidLoad function.
let gesture = UIPanGestureRecognizer(target: self, action: #selector(type(of: self).wasDragged(gestureRecognizer:)))
optionsView.addGestureRecognizer(gesture)
finally add this callback and 2 functions:
#objc func wasDragged(gestureRecognizer: UIPanGestureRecognizer) {
let distanceFromBottom = screenHeight - gestureRecognizer.view!.center.y
if gestureRecognizer.state == UIGestureRecognizer.State.began || gestureRecognizer.state == UIGestureRecognizer.State.changed {
optionsOpenedConstraint.isActive = false
optionsVisiableConstraint.isActive = false
let translation = gestureRecognizer.translation(in: self.view)
if((distanceFromBottom - translation.y) < 100) {
gestureRecognizer.view!.center = CGPoint(x: gestureRecognizer.view!.center.x, y: gestureRecognizer.view!.center.y + translation.y)
gestureRecognizer.setTranslation(CGPoint(x: 0, y: 0), in: self.view)
}
}
if gestureRecognizer.state == UIGestureRecognizer.State.ended{
if distanceFromBottom > 6{
openOptionsPanel()
}else{
closeOptionsPanel()
}
}
}
func openOptionsPanel(){
optionsOpenedConstraint.isActive = true
optionsVisiableConstraint.isActive = false
UIView.animate(withDuration: 0.5) {
self.view.layoutIfNeeded()
}
}
func closeOptionsPanel(){
optionsOpenedConstraint.isActive = false
optionsVisiableConstraint.isActive = true
UIView.animate(withDuration: 0.5) {
self.view.layoutIfNeeded()
}
}
and voalá
#IBAction func handlePanGesture(_ recognizer: UIPanGestureRecognizer) {
if MainView.bounds.contains(mainImage.frame) {
let recognizerCenter = recognizer.location(in: MainView)
mainImage.center = recognizerCenter
}
if MainView.bounds.intersection(mainImage.frame).width > 50 && MainView.bounds.intersection(mainImage.frame).height > 0 {
let recognizerCenter = recognizer.location(in: MainView)
print(recognizerCenter)
mainImage.center = recognizerCenter
}
}
#IBAction func handlePinchGesture(_ recognizer: UIPinchGestureRecognizer) {
mainImage.transform = mainImage.transform.scaledBy(x: recognizer.scale, y: recognizer.scale)
recognizer.scale = 1.0
}
#IBAction func handleRotateGesture(_ recognizer: UIRotationGestureRecognizer) {
mainImage.transform = mainImage.transform.rotated(by: recognizer.rotation)
recognizer.rotation = 0.0
}

In iOS, how to drag down to dismiss a modal?

A common way to dismiss a modal is to swipe down - How do we allows the user to drag the modal down, if it's far enough, the modal's dismissed, otherwise it animates back to the original position?
For example, we can find this used on the Twitter app's photo views, or Snapchat's "discover" mode.
Similar threads point out that we can use a UISwipeGestureRecognizer and [self dismissViewControllerAnimated...] to dismiss a modal VC when a user swipes down. But this only handles a single swipe, not letting the user drag the modal around.
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.
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
}
}
}
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!
})
}
}
}
}
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;
}
}
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
}
}
created a demo for interactively dragging down to dismiss view controller like snapchat's discover mode. Check this github for sample project.
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
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.
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!)
}
}
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!
})
}
}
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.
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.
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];
}
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!
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)
}
}
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.
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)
}
}
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.

Resources