Protocol Extensions in Swift 2.0 – Draggable UIViews

Protocol extensions in Swift 2.0 rule

One of the features I was most excited about when Swift 2.0 was announced was the ability to provide an extension for a protocol. This means that you can provide a default implementation for an object that adheres to a protocol. Think of this like a ruby mixin!

A real world example

What does this look like? Let’s start with something simple: we want to provide a common protocol to make things draggable.  We’ll define the Draggable protocol to have the following:

protocol Draggable: class {
    var view: UIView { get }
    var initialLocation: CGPoint { get set }

    func registerDraggability() -> Void
    func removeDraggability() -> Void
    func didPress(pressGesture: UILongPressGestureRecognizer) -> Void
    func didPan(panGesture: UIPanGestureRecognizer) -> Void
}
  • view is is the actual UIView we want to move
  • initialLocation is the starting center when dragging
  • registerDraggability() is called when we want our class to start listening to dragging
  • removeDraggability() is the opposite, where say we no longer want our class  to be draggable anymore
  • didPress and didPan are the two functions to handle the actual pressing and panning

Normally, this would be pretty straight forward: You would create a UIView subclass that would adopt the Draggable protocol to which you’d then implement the necessary properties and functions.

This works great until you need to do this for another subview. So then you repeat: subclass, adopt and implement. All of the sudden, you’re writing what’s basically the same code over and over. That’s bad! As engineers, we want to write code that’s DRY (don’t repeat yourself) so whats our first take at rectifying this?

Composition vs. Inheritance

If your first response was to create a baseclass that adopts and implements the Draggable protocol, you wouldn’t be wrong and infact that’s a perfectly sensible approach.

Inheritance is a powerful and a tried and true programming practice that has its place; however, it is possible to get into hierarchy hell. I know I’ve run into the issue of having too many UI classes that inherit from each other and making one small change in a base class ends up affecting WAY too many of its descendants.

This is where composition shines!

Instead of having a long chain of subclasses that can be somewhat brittle, we’d like to compose our objects to adhere to our various protocols. This becomes very viable with protocol extensions as we now can provide a default implementation of a protocol:

extension Draggable where Self: UIView {
}

The real beauty of this protocol extension is we’re giving ourselves access to any UIView that adopts the Draggable protocol! How awesome is that? For example, you remember that view property in the Draggable protocol that is required to be implemented? Now we can just do this:

extension Draggable where Self: UIView {
    var view: UIView { get { return self } }
    var parentView: UIView? { get { return self.view.superview } }
}

Basically, any UIView that has the Draggable protocol automatically receives the built in functionality. So what about the remainder of the protocol’s definition? Let’s fill them out:

func registerDraggability() {
    let panGesture = UIPanGestureRecognizer()
    panGesture.handler = { gesture in
        self.didPan(gesture as! UIPanGestureRecognizer)
    }
    
    self.view.addGestureRecognizer(panGesture)
    
    let pressGesture = UILongPressGestureRecognizer()
    pressGesture.minimumPressDuration = 0.001
    pressGesture.handler = { gesture in
        self.didPress(gesture as! UILongPressGestureRecognizer)
    }
    
    self.view.addGestureRecognizer(pressGesture)
}

func removeDraggability() {
    guard self.gestureRecognizers != nil else {
        return
    }
    
    let _ = self.gestureRecognizers!
        .filter({ $0.delegate is UIGestureRecognizer.GestureDelegate })
        .map({ self.removeGestureRecognizer($0) })
}

func didPress(pressGesture: UILongPressGestureRecognizer) {
    switch pressGesture.state {
    case UIGestureRecognizerState.Began:
        self.initialLocation = self.view.center
        UIView.animateWithDuration(0.1, animations: { () -> Void in
            self.parentView?.bringSubviewToFront(self.view)
            self.view.transform = CGAffineTransformMakeScale(0.80, 0.80)
        })
        break
    case UIGestureRecognizerState.Cancelled, UIGestureRecognizerState.Ended, UIGestureRecognizerState.Failed:
        UIView.animateWithDuration(0.1, animations: { () -> Void in
            self.view.transform = CGAffineTransformMakeScale(1.0, 1.0)
        })
        break
    default:
        break
    }
}

func didPan(panGesture: UIPanGestureRecognizer) {
    let translation = panGesture.translationInView(self.parentView)
    self.view.center = CGPointMake(self.initialLocation.x + translation.x, self.initialLocation.y + translation.y)
}

I don’t believe I need to go over the code to drag a UIView as that’s been covered dozens of times, but there are some caveats that I’d like to bring up:

I had to extend the UIGestureRecognizer class in order to add a method called handler. Tyler Tillage over at CapTech did an excellent job explaining why I couldn’t just use the existing addTarget: action: in the protocol extension here. Long story short:

UIKit is still compiled from Objective-C, and Objective-C has no concept of protocol extendability. What this means in practice is that despite our ability to declare extensions on UIKit protocols, UIKit objects can’t see the methods inside our extensions.

To get around this, my handler closure wraps around some object associations to store the closure which is then called on a default selector. I’ll write another post about why I had to do all of this, but feel free to check out this extension.

Just tell me what I need to do to make things draggable!

When all is said and done, we want it to be as easy as pie to make ANY UIView draggable. I feel like I’m almost there with this:

class DraggableView: UIView, Draggable
{
    var initialLocation: CGPoint = CGPointZero
    
    override func didMoveToSuperview() {
        if self.superview != nil {
            self.registerDraggability()
        } else {
            self.removeDraggability()
        }
    }
}

All you need to do is register the draggability on the view, and provide a storage backing for the view’s initial location before it starts dragging. Pretty simple right?

The best part of this is it can now be applied to other, UIView backed classes! How about UICollectionViewCell?

class DraggableCell: UICollectionViewCell, Draggable {
    
    var initialLocation: CGPoint = CGPointZero
    
    override func didMoveToSuperview() {
        if self.superview != nil {
            self.registerDraggability()
        } else {
            self.removeDraggability()
        }
    }
}

Remember, we’re not creating a single DraggableView baseclass that requires all of our other classes to subclass. We’re simply adding the additional functionality provided by the protocol and its default extension.

So what does this look like in application?

protocol extension uiview dragging protocol extension uicollectionviewcell dragging

 

Closing thoughts…

I don’t think I’m sold on this implementation yet. I’m not a fan of object association as some of it feels dirty but in the end it does work, pretty well at that too. The biggest limitation right now is that extensions cannot store additional values and they do not support really anything regarding selectors. Once those two features are put in place, we’ll really be able to do some even more amazing things without having to jump through loops.

Feel free to check out the repository on GitHub and let me know what you think! I’d love to clean this up more if anyone has better ideas!

Leave a Reply

Your email address will not be published. Required fields are marked *