Using closures as event handlers in iOS

Swiftify your APIs

Originally posted on October 31, 2015
Updated on

Target-action

If you're new to iOS development, you might be excited by the prospect of writing apps using Apple's awesome new Swift programming language. That excitement might take a hit once you find out that the frameworks you'll be using are much older than Swift and don't fit this new language at all. This week, I encountered yet another example of this: the so-called target-action pattern. This is the pattern used for event handling on iOS. Instead of assigning an object (conforming to some protocol or being of some function type) as an event handler, you specify a target object and an action, the name of a method to call on that object:

control.addTarget(someObject, action: "someMethod", forControlEvents: .SomeEvent)

Wait, what, is that a String instead of a typed object? That's not at all what you'd expect to be doing in Swift. Not only does this approach feel outdated, it has severe limitations. The following example should illustrate this.

Do it yourself

In this example, we generate a list of views, in code, based on a collection of objects in a model. A do-it-yourself UITableView if you wish. The model consists of a simple list of names, a [String]. When the view loads, it should show a list of names, with a button next to each of them. Clicking a button should show a greeting that uses the name that is next to the button. The resulting application should look as follows.

Screenshot

The code for this view's controller is as follows. Note that I simplified and summarized this code. For the full source code, download the Xcode project from GitHub.

let names = ["Alice", "Bob", "Charlie", "David"]

override func viewDidLoad() {
  for name in names {
    /* Create a label and a button, and add them to the view */
    button.addTarget(self, action: "showGreeting", forControlEvents: .TouchUpInside)
  }
}

func showGreeting() {
  /* Update the greeting */
}

Unfortunately, this code doesn't work. The showGreeting method has no way of knowing what the name next to the button is. While there are ways to work around this problem, none of these workarounds result in elegant code. The cause of this problem lies with the target-action pattern. All this pattern allows us to tell the button is what method to call. Other than the sender of the action and some event object, this method has no way of knowing what happened. If we could use a closure instead of an action method, that closure could capture the current name and use it to update the greeting.

And so I put on my thinking cap and tried to come up with a way to use closures as event handlers for UIControls. What follows is a summary of the steps I took in designing a utility class that adds support for closures to UIControl.

Wrap it

Since there is no way around the target-action pattern, we need to wrap the closure in a target object and provide an action method. Let's start with the following class:

class ClosureHandler: NSObject
{
  private let code: Void -> Void

  init(code: Void -> Void) {
    self.code = code
    super.init()
  }

  func action() {
    code()
  }
}

This class allows us to re-write our addTarget command as follows:

button.addTarget(ClosureHandler(code: { /* Update greeting */ }), action: "action", forControlEvents: .TouchUpInside)

Our ClosureHandler is now used as the target object and its action method will execute the closure we provided in the initializer. The closure can capture the local variable name and use it to update the greeting, solving our problem. Awesome!

The code can be made more concise by giving the class a shorter name (let's pick λ) and using trailing closure syntax.

button.addTarget(λ { /* Update greeting */ }, action: "action", forControlEvents: .TouchUpInside)

Further enhancements can be made by extending UIControl to add a more Swift-like method:

public extension UIControl
{
  public func addAction(controlEvents: UIControlEvents, action: Void -> Void) {
    addTarget(λ(code: action), action: "action", forControlEvents: controlEvents)
  }
}

With this extension in place, our λ class now does its work behind the scenes. Adding event handlers can now be done as follows:

button.addAction(controlEvents: .TouchUpInside) {
  /* Update greeting */
}

Retain it

If you tried out the previous technique, you will have noticed that nothing seems to happen when you click the button. To find out why, read the documentation for the addTarget:action:forControlEvents: method. The documentation tells us that this method does not retain the target object, meaning our λ objects will die long before they get a chance to do their job. Poor objects.

An obvious solution to this problem is to store the objects somewhere in the controller class, but that solution would be tedious. Surely, we can do better. An ideal solution would not require any additional work from the user of our λ class. A possible solution would be to make λ a subclass of UIView and store a λ object as a subview of the control that targets it, but I consider that an ugly hack. Since don't want the class that uses a λ object to have to retain it and we have no clean way of storing it inside the control that uses it, the only elegant solutions seems to require that the λ object retain itself. This is possible by deliberately introducing a reference cycle:

class λ: NSObject
{
  private let code: Void -> Void
  private var keepAlive: (Void -> Void)?

  init(code: Void -> Void) {
    self.code = code
    super.init()
    keepAlive = { self }
  }

  func action() {
    code()
  }
}

The keepAlive closure captures a reference to self, thereby retaining it. Our λ objects will now stay in memory. If you're wondering why keepAlive is optional: this closure needs to capture self so it can only be initialized after self is available, which is in phase two of two-phase initialization. The property still needs a value in phase one - otherwise we aren't allowed to enter phase two - and I chose to set it to nil.

In the next section, I'll discuss another reason why I chose to make keepAlive optional.

Release it

While the keepAlive property does solve our problem of keeping λ objects in memory, it does introduce a reference cycle, retaining the λ object indefinitely. We would sleep better at night knowing λ objects are released when they are no longer needed. We can achieve this by introducing a symmetrical removeAction(controlEvents:) method. This method provides an alternative for removeTarget:action:forControlEvents: as well as a time and place to solve our reference cycle problem.

First, we add a remove method to class λ in which we set keepAlive to nil, breaking the reference cycle and releasing the λ object.

func remove() {
  keepAlive = nil
}

Next, we add a removeAction(controlEvents:) method to our UIControl extension. This method will remove all λ objects from the control's dispatch table as well as release them.

public func removeAction(controlEvents: UIControlEvents) {
  for target in allTargets() {
    if let target = target as? λ {
      removeTarget(target, action: "action", forControlEvents: .TouchUpInside)
      target.remove()
    }
  }
}

Further enhancements

Further enhancements can be made by giving each λ object a name. This would allow us to selectively remove λ objects from a control's dispatch table, instead of removing them all at once. This enhancement has been implemented in the example on GitHub.

If you've enjoyed my work or found it helpful, please consider becoming a patron. Your support helps me free up time to work on my books and projects.