Saturday, September 27, 2014

Swift: Fun with operators and delegation (updated)

Apple's new programming language has a quite extensive bag of tricks to make you more productive.
One of the more controversial ones is the ability to define custom operators.

Apple's official guide has a short overview of custom operators and is recommended reading.

Let's look at an example of how operators can be useful (misused?) in the context of iOS programming.

How would you read this?

pickerView --> myDelegate

Read on to confirm your hypothesis.

Pick me up


It is quite common to setup your view controller as delegate as well as data source for components like UITableView, UIPickerView, UICollectionView etc.

In Storyboard you simply drag the delegate/datasource connector to your view controller and you are done.
This is all fine and dandy, but becomes smelly if you are using two or more views of the same type,
for example, two UIPickerViews.

Looking at the solutions that were proposed on StackOverflow, a common trick is to either use tags or compare outlets in the common implementation of the delegate or data source methods like this:

// returns the number of 'columns' to display.
    func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int {
        if (pickerView == picker1) {
             return 4
        } else if (pickerView == picker2) {
             return 1
        }
// or using switch
//        switch pickerView {
//            case timer: return 4
//            default: // you need a default case
//                return 1
//        }
    }
    

Might get ugly fast. Wouldn't it be nice to have two delegates/data sources. One for each view, that is still somehow part of your view controller? Also, wouldn't it be nice to easily move this logic to a different view controller, if necessary?

I didn't find an elegant way to solve this in Storyboard.
(UPDATE: Someone pointed out I can add any kind of object to a Storyboard and wire those as delegate/data source. That's cool! I still would like to have my view controller code in one place and can't use inner classes in Storyboard)

Also, there's no common protocol implemented by UIViews that indicate they have delegate/datasource properties to set.

Here's a solution that uses inner classes to act as delegates/data sources as well as a new operator that connects views with their delegates/data sources just for funsies.

First the new custom operator which takes care of setting up the delegation chain.

import UiKit

infix operator --> { associativity left }
func --> (left: NSObject, right: NSObject) {
    if (left.respondsToSelector("delegate")) {
        left.setValue(right, forKey: "delegate")
    }
    if (left.respondsToSelector("dataSource")) {
        left.setValue(right, forKey: "dataSource")
    }
}

This creates a new infix operator '-->' which takes care of delegation for you.
It uses key-value coding offered by NSObject to determine if the left hand side of the operator supports a delegate and/or data source and then assigns the right hand side accordingly.
The operator is defined with left associativity and the default precedence value (100).

Note that the current version of Swift doesn't offer much in terms of reflection or metadata programming. Luckily NSObject offers the necessary machinery behind the scenes for us. Also note how selector strings are implicitly converted to Selector objects (since Selector implements StringLiteralConvertible)!

Next up is a delegate/data source for UIPickerView. Note that it has to inherit from NSObject.

    class PickerDelegate : NSObject, UIPickerViewDelegate, UIPickerViewDataSource {
        func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int {
            return 1
        }
        func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
            return 10
        }
        func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String! {
            return "Pick me \(row)"
        }
    }

Now consider adding one or more of those as inner classes to your UIViewController.
In order to wire everything together, two things are missing:
  1. Creating an instance of the delegate
  2. Setting up the delegate/data source chain in viewDidLoad
...
   @IBOutlet weak var picker: UIPickerView! // hook this up via Storyboard
   // 1
   let pickerDelegate = PickerDelegate()

   //2
   override func viewDidLoad() {
        super.viewDidLoad()
        picker --> pickerDelegate   
    }
    ...
Note that if you don't store a reference to the delegate in your view controller, the garbage collector might throw it away since UIViews are not hanging on to them (the property is marked as unowned(unsafe)). The same is true for the datasource property.

I also attached a complete storyboard that shows the general idea.

Advantages

  • Clear separation of concerns. If two or more views of the same type are associated with your ViewController, you will enjoy not having to write if-else construct or switch statements
  • Easy to refactor. If you decide to move your views to a different view controller, it's easy to move the delegate. If you have it in its own top-level file, it is even easier (than using inner classes)
  • Expressive syntax that indicates calls going from the UIView to a delegate/data-source

Disadvantages

  • Code in operator is relying on reflection.
  • Not much is gained and regular code is type-safe:
  • picker.delegate = pickerDelegate
       picker.datasource = pickerDelegate
       
  • Custom operators are global. Beware of clashes.

Note that I'm still pretty new to Swift development and especially Xcode 6. Let me know if you've found more elegant solutions.

But, but it isn't type safe! - Protocol composition to the rescue


Yes, key-value programming is not type safe.
And unfortunately, there is no protocol defined for UIViews that have a delegate and or datasource.

In order to make the code of the operator type-safe, we will have to define the --> function for every UIView that supports those patterns.
Here's an example for UIPickerView.

   func --> (left:UIPickerView, right: protocol<UIPickerViewDelegate,UIPickerViewDataSource>) {
       left.delegate = right
       left.dataSource = right
   }

Notice the usage of protocol composition for the right hand side of the operator.


And here's the link to the Playground.