Written by Alex Gibson, on April 27, 2017


Drag Down to Dismiss in Swift

Users have become accustomed to certain actions inside of apps and one of those actions is the ability to dismiss a view by dragging it down. Whether the user learned this from Facebook or Twitter to keep your app fluid you may want to implement this on your detail views. This tutorial is an extension of Animated Transitions in Swift and the Draggable UIView tutorial. I strongly recommend you work through those first.  The Draggable UIView will be needed as well as the animated transition.

To build up to this tutorial I recommend you complete the Draggable UIView tutorial here as well as the custom animated transitions tutorial with the link here.
Download the Drag Down to Dismiss starter project at Apptillery Github.  Run the project and click on any image and you should see a smooth transtion from the cell to the detail view.  The code in the animated transition is almost identical to the Animated Transitions Tutorial with only a few modifications to allow over context presentations.  We want to add the ability to swipe down to dismiss this ImageViewController.  We are going to use the Draggable UIView that we already made to do this.  If you don’t have one built yet complete the tutorial.

Now drag your Draggable UIView that was previously created into the project.  We are going to make a couple of changes.  First in the ImageDetailViewController change the containerView property to type DraggableView


@IBOutlet weak var containerView: DraggableView!

Go to storyboard and change the containerView to type DraggableView.

 

 

Back in our ImageDetailViewController let’s do two things.  First, let’s declare an extension at the bottom of the file for our DraggableView delegate.


extension ImageDetailViewController:DraggableViewDelegate{
    
}

Second, in the viewDidLoad set the delegate of our containerView to the controller.


containerView.delegate = self

To pull off what we want, we need to know when the user is dragging the container and change the position and we need to fade the main view to reveal the previous controller behind our detailviewcontroller.  In the extension declare the needed delegate methods from our draggable view.


extension ImageDetailViewController:DraggableViewDelegate{
    func panGestureDidChange(_ panGesture: UIPanGestureRecognizer, originalCenter: CGPoint, translation: CGPoint, velocityInView: CGPoint) {
        
    }
    func panGestureDidEnd(_ panGesture: UIPanGestureRecognizer, originalCenter: CGPoint, translation: CGPoint, velocityInView: CGPoint) {
        
    }
}

In didChange we want to update the position of the container and fade the view.  I would rather the user only be able to move the view up and down and therefore I am going to log in the x value.  We also want to fade the main view based on how far the user drags down the containerView.


func panGestureDidChange(_ panGesture: UIPanGestureRecognizer, originalCenter: CGPoint, translation: CGPoint, velocityInView: CGPoint) {
        containerView?.frame.origin = CGPoint(
            x: containerView!.frame.origin.x,
            y: translation.y
        )
        
        if containerView.center.y > originalCenter.y {
            let alpha = 1 - (abs(self.view.bounds.height/2 - containerView.center.y)/(self.view.bounds.height))
            self.view.alpha = alpha
        }else{
            self.view.alpha = 1
        }
    }

Now we need to make the view snap back if the user did not drag the view far enough or make it dismiss.  We will do that in panDidEnd


func panGestureDidEnd(_ panGesture: UIPanGestureRecognizer, originalCenter: CGPoint, translation: CGPoint, velocityInView: CGPoint) {
        if containerView.center.y >= containerView.bounds.height * 0.66{
            if let _ = transitioningDelegate{
                self.dismiss(animated: true, completion: nil)
            }else{
                //handle non custom presentation
                UIView.animate(withDuration: 0.3, animations: {
                    self.containerView.frame.origin.y = self.view.bounds.height
                }, completion: { (finished) in
                    self.dismiss(animated: false, completion: nil)
                })
            }

        }else{
            UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.9, options: .curveEaseInOut, animations: {
                self.containerView.center = originalCenter
                self.view.alpha = 1
            }, completion: nil)
        }
    }

You can see if the user does not drag the center of the containerView passed a y value of 66% of the height then it will snap back using the animation to the original position.  If you run the project it will work but to work the way we want I recommend you remove the code inside the DraggableView that we built that changes the frame for itself.  This will keep the view from going side to side when the user drags.  The code is specifically in the .changed case of the DraggableView subclass.


self.frame.origin = CGPoint(
                x: originalPosition!.x - self.bounds.midX + translation.x,
                y: originalPosition!.y  - self.bounds.midY + translation.y
            )

Delete this and the view will no longer move side to side but only up and down as we defined in our extension of the ImageDetailViewController.

Run the project.  Now all seems good except we have faded the main view and it is also fading all the views that it contains.  Let’s add a view in the back that can be faded while keeping the image alpha at 1.  At the top of the controller declare a view and call it fadeView.


var fadeView : UIView?

In the viewDidLoad let’s allocate it, set the backgroundColor to black, and add to the view at the back of all views.  I am also changing the main view color to clear.


self.view.backgroundColor = .clear
        fadeView = UIView(frame: view.bounds)
        fadeView?.autoresizingMask = [.flexibleWidth,.flexibleHeight]
        fadeView?.backgroundColor = .black
        if let fv = fadeView{
            self.view.insertSubview(fv, at: 0)
        }

 

Now simply change all the self.view.alpha changes in the extension to self.fadeView?.alpha and the dismiss should be a lot more presentable.

 

Run and enjoy. The completed project is on the Apptillery GitHub.