Written by Alex Gibson, on April 15, 2017


If you have ever wanted to have a draggable UIView you know that it takes up a lot of code in a UIViewController. In this tutorial we are going to work on creating a draggable UIView that handles a lot of the work so we can keep our controller small and lean.  We could do this two different ways with one being protocol oriented programming and the later being a traditional subclass.  I am going to use the traditional subclass.  Doing this requires less work and we can create an optional delegate to notify our controller or model.  This tutorial will also be needed if you plan on doing the Drag Down to Dismiss Tutorial that is in the works.

Download the starter project from Apptillery Github.

Take a look around.  You should see we already have a DraggableView subclass and there is one view in the storyboard.  Go to the storyboard and change the blue view to a DraggableView.

 

Open the DraggableView subclass file and let’s work on making it really draggable.  We will need a couple of properties straight away.  One is the original position and the other is the UIPanGestureRecognizer.  Declare these at the top of the DraggableView.


var panGestureRecognizer : UIPanGestureRecognizer?
var originalPosition : CGPoint?

Next, we need the required methods to instantiate our view in storyboard or in code.


override init(frame: CGRect) {
        super.init(frame: frame)
        setUp()
    }
    
required init?(coder aDecoder: NSCoder) {
       super.init(coder: aDecoder)
       setUp()
    }

 

We have not declared setUp() so let’s do that now.  We are need to instantiate the panGestureRecognizer and make sure the view is user interaction enabled.


func setUp(){
        isUserInteractionEnabled = true
        panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(panGestureAction(_:)))
        self.addGestureRecognizer(panGestureRecognizer!)
    }

This is straight forward enough. Now we need a panGestureAction function to match the one from the setup.  We also need to track the state of the gesture so below setup add the following declaration and switch statement.


func panGestureAction(_ panGesture: UIPanGestureRecognizer) {
        let translation = panGesture.translation(in: superview)
        let velocityInView = panGesture.velocity(in: superview)
        
        switch panGesture.state {
        case .began:
            break
        case .changed:
            break
        case .ended:
            break
        default:
            break
        }
    }

If you have ever used a pan gesture this will make sense.  We can track the gesture with the current gesture.state and translation and velocity.  We need the translation in the superview and we need the velocity at times to determine how fast the user is panning in a direction.  We also need to determine the state to execute different actions.  What we want is for the DraggableView to know it’s own initial location in began and handle actions in began, changed and ended.

So in state began let’s track the initial center of the view and in the changed case of the switch we are going to change the origin of the frame by the translation. We have to adjust the origin using originalPosition and since the origin is not the center we have to adjust it with half the bounds vertical and horizontal size.


switch panGesture.state {
        case .began:
            originalPosition = self.center
            break
        case .changed:
            self.frame.origin = CGPoint(
                x: originalPosition!.x - self.bounds.midX + translation.x,
                y: originalPosition!.y  - self.bounds.midY + translation.y
            )
            break
        case .ended:
            break
        default:
            break
        }

We now have a draggable UIView subclass.  Run the project and you should be able to drag the blue view around. Notice it stops wherever you stop dragging.  You might want it to snap back or trigger something else.

We are going to implement some delegate functions so that you can add behaviors to the different states.  You can use these functions in a UIViewController, UIView,ViewManager, or a subclass of the DraggableView.  That is why this will be important.  I have already declared the protocol but we need to add a weak delegate variable below our panGesture variable and our originalPosition variable.


weak var delegate : DraggableViewDelegate?

Let’s move inside the protocol and declare the functions.  Personally, I would like the functions to be optional so add @objc above the class declaration.  This will allow you to only implement the functionality you need in the future without having to declare all functions each time.

You might want different information from your DraggableView, but the information that I am interested in would be the gesture state, velocity in the view, original position, and the gesture object as well.  We will also have a delegate that handles the default case of our switch.


@objc
protocol DraggableViewDelegate: class {
    @objc optional func panGestureDidBegin(_ panGesture:UIPanGestureRecognizer, originalCenter:CGPoint)
    @objc optional func panGestureDidChange(_ panGesture:UIPanGestureRecognizer,originalCenter:CGPoint, translation:CGPoint, velocityInView:CGPoint)
    @objc optional func panGestureDidEnd(_ panGesture:UIPanGestureRecognizer, originalCenter:CGPoint, translation:CGPoint, velocityInView:CGPoint)
    @objc optional func panGestureStateToOriginal(_ panGesture:UIPanGestureRecognizer,originalCenter:CGPoint,  translation:CGPoint, velocityInView:CGPoint)
}

These delegates should allow you to handle anything you would want to do such as snapback or trigger a progress.  Let’s add them to our switch statement in the DraggableView.  Notice all you have to do is use them in each case of the switch.  The completed function


func panGestureAction(_ panGesture: UIPanGestureRecognizer) {
        let translation = panGesture.translation(in: superview)
        let velocityInView = panGesture.velocity(in: superview)
        
        switch panGesture.state {
        case .began:
            originalPosition = self.center
            delegate?.panGestureDidBegin?(panGesture, originalCenter: originalPosition!)
            break
        case .changed:
            self.frame.origin = CGPoint(
                x: originalPosition!.x - self.bounds.midX + translation.x,
                y: originalPosition!.y  - self.bounds.midY + translation.y
            )
            delegate?.panGestureDidChange?(panGesture, originalCenter: originalPosition!, translation: translation, velocityInView: velocityInView)
            break
        case .ended:
            delegate?.panGestureDidEnd?(panGesture, originalCenter: originalPosition!, translation: translation, velocityInView: velocityInView)
            break
        default:
            delegate?.panGestureStateToOriginal?(panGesture, originalCenter: originalPosition!, translation: translation, velocityInView: velocityInView)
            break
        }
    }

So now we can use our protocol.  I will show you a quick example of how to use it cleanly by creating another subclass of our DraggableView.  Go to File->New->File->Cocoa Touch Class.  Make it a subclass of our DraggableView and name it SnapDraggable.

Go to the storyboard and change the blue view to be of type SnapDraggable.

Add the code to SnapDraggable that will make the SnapDraggable view snap back to it’s original position after panning ends.  Here is the complete SnapDraggable.


import UIKit

class SnapDraggable: DraggableView,DraggableViewDelegate {
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        delegate = self
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        delegate = self
    }
    
    func panGestureDidEnd(_ panGesture: UIPanGestureRecognizer, originalCenter: CGPoint, translation: CGPoint, velocityInView: CGPoint) {
        UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.9, options: .curveEaseInOut, animations: {
            self.center = originalCenter
        }, completion: nil)
    }

}



I would probably rather have a view manager handle this but this was just a quick way to show how you can extend what we have built.  Run the project and drag the blue view. It should snap back to the original location after you stop panning.  Since we have delegate functions that we can implement we can now use these with our DraggableView and do many different actions.  We can fade based on a threshold, drive animations, or fire an action after the view has left the screen.  The possibilities are endless.  Happy coding.