Callbacks, Part 2: Closure, Target-Action, and Responder chain

2018, Feb 08    
Callbacks, Part 2: Closure, Target-Action, and Responder chain

This article continues the series of posts about callback techniques in Cocoa, their comparison & benchmarking. If you’re familiar with the concepts of Closure, Target-Action and Responder chain, you might want to skip the introduction and go straight to the Pros & Cons of each. Happy reading!

Callbacks, Part 1: Delegation, NotificationCenter, and KVO

Callbacks, Part 3: Promise, Event, and Stream (Functional Reactive Programming)

Closure (Block)

Although this series of the posts is called “Callbacks in Cocoa”, the word “callback” is usually used as a synonym for Closure (or Block).

Closure and Block are two names for the same phenomenon used in Swift and Objective-C respectively.

The documentation says that the “Closures are self-contained blocks of functionality that can be passed around…“.

If you think about it, regular objects of any class are also “self-contained blocks of functionality” that can be “passed around” by the reference, but Closure is its own beast.

Just like objects, a Closure has associated “functionality”, that is some code to be executed, and also Closure can capture variables and operate with them as would an object do with its own variables.

The key difference is that the functionality of an object is declared by its class implementation, which is a standalone structure, but for Closure, we write its implementation in place of use, where it’s passed somewhere else as a parameter.

Although a Closure has characteristics of an object, to a bigger extent it is a function. Because this function is declared without association to a type, it is also called anonymous function.

The easiest way to grasp the concept is to see an example, and if we are to solve the Pizzeria problem from the previous post, we could end up with the following code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// The Closure type can be pre-defined, and since it's an anonymous function, its declaration
// looks like a function signature. In our case - a void function that takes a Pizza
typealias PizzaClosure = (Pizza) -> Void

class Pizzaiolo {

  // "An instance" of the PizzaClosure can be treated just as an object
  private var completion: PizzaClosure?

  // PizzaClosure will be passed in as a parameter
  func makePizza(completion: PizzaClosure) {
    // Save the 'completion' to be able to call it later
    self.completion = completion
    // Cooking the pizza
    ...
  }
  
  private func onPizzaIsReady(pizza: Pizza) {
    // Calling the closure in order to "deliver" the pizza
    self.completion?(pizza)
    self.completion = nil
  }
}

class Customer {

  func askPizzaioloToMakeAPizza(pizzaiolo: Pizzaiolo) {
    // We declare the body of the closure in a place where it is passed as an input parameter
    pizzaiolo.makePizza(completion: { pizza in
      // We're inside the closure! And we've got the pizza!
      // This code gets executed when pizzaiolo calls the closure we passed to him
    })
  }
}

Advantages of using Closure (Block)

  • Loose coupling - the callee and caller must agree only on the signature of the closure, which is the weakest connascence of name.
  • High cohesion of the business logic - the action and the response to the action can be declared together in one place.
  • Capturing of the variables from the client’s context helps with state management because we don’t need to store the variables in the class and face potential access problems, such as the race condition.
  • Closures are as fast as calling a regular function.
  • Just like with delegation, Closure can have non-void return type, so it also can be used as a DataSource for asking for data from the client code
  • Full type safety. Using the static code analysis the IDE is able to check the signature of the closure and verify types of parameters passed in.

Disadvantages of using Closure (Block)

  • Closures and blocks have quite complex syntax
  • For newcomers the concept of anonymous function (or lambda) is harder to understand than most constructions from imperative programming
  • Because Swift and Objective-C use automatic reference counting memory management, closures can cause memory leaks when they form so-called retain cycle. We should always be careful when using Closures and use [weak ...] modificator in cases where the cyclic capture takes place
  • When used as callbacks for async operations, in practice the client code can often result in several nested in each other callbacks, which is called the pyramid of doom, which is hard to read and support. Moreover, when the code flow inside the callbacks has branching (if - else) the pyramid size grows exponentially.
  • Arguments of the closure or block cannot have argument labels. Because of that, the client code should assign names to each argument, which may lead to a wrong interpretation of the parameters after refactoring of the closure signature.
  • [Obj-C] Blocks must be checked for NULL before being called, or the app will crash.

Target-Action (NSInvocation)

Target-action on its own is a design pattern, but conceptually it’s a variation of the command. Traditionally this callback technique is used in Cocoa for handling user interactions with the UI but is not restricted to this use case.

The client code provides two parameters in order to receive a callback in the future - an object and a function. In this context, the object is target, and the function is action.

The callback then is fulfilled by calling the action function on the target object.

It’s not a trivial case for a programming language when an object and its function are provided separately, so this technique requires support from the language.

For example, Swift does not allow us to do the trick of calling an arbitrary method on an arbitrary object, so we’d need to appeal to Objective-C if we wanted something like that to work in Swift project.

On the other hand, there are a few ways how we can perform the action on the target in Objective-C (thanks to volatile Objective-C runtime). In this example, I’m using the class NSInvocation, which suits our needs the best.

Implementation of the Pizzeria problem with target-action would look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
@interface Pizzaiolo : NSObject

// The target and the action are sent separately
- (void)makePizzaForTarget:(id)target selector:(SEL)selector;

@end

@interface Customer: NSObject

- (void)askPizzaioloToMakePizza:(Pizzaiolo *)pizzaiolo;

@end

// ------ 

@implementation Pizzaiolo

- (void)makePizzaForTarget:(id)target selector:(SEL)selector
{
  NSInvocation *invocation = [self invocationForTarget:target selector:selector];
  // Storing the reference to the invocation object to be used later
  self.invocation = invocation;

  // making pizza
  ...
}

- (void)onPizzaIsReady:(Pizza *)pizza
{
  // Argument indices start from 2, because indices 0 and 1 indicate the hidden arguments self and _cmd
  if (self.invocation.methodSignature.numberOfArguments > 2) {
    [self.invocation setArgument:&pizza atIndex:2];
  }
  [self.invocation invoke];
}

// A helper method that constructs NSInvocation for given target and action
- (NSInvocation *)invocationForTarget:(id)target selector:(SEL)selector
{
  NSMethodSignature *signature = [target methodSignatureForSelector:selector];
  if (signature == nil) return nil;
  NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
  invocation.target = target;
  invocation.selector = selector;
  return invocation;
}

@end

@implementation Customer

- (void)askPizzaioloToMakePizza:(Pizzaiolo *)pizzaiolo
{
  [pizzaiolo makePizzaForTarget:self selector:@selector(takePizza:)];
}

- (void)takePizza:(Pizza *)pizza
{
  // Yeah! Objective "pizza" completed
}

@end

Advantages of using Target-Action

  • In some aspects target-action is similar to delegation, but has two great advantages over it:
    • The name of the callback function is not predefined and is to be provided by the client code. This greatly decouples the caller and the callee.
    • It supports multiple recipients with individual methods to be called for each of them.
  • NSInvocation provides a way to get a non-void result of invocation, so this technique can be used for requesting data (but I would not recommend to do it - read the disadvantages).
  • Static code analyzer is able to perform at least shallow check of the method name provided as action. It cannot tell if the target is actually implementing the action.

Disadvantages of using Target-Action

  • Complex infrastructure. For target-action we need to implement everything ourselves:
    • Constructing NSInvocation the right way
    • Managing a collection of NSInvocation in order to support multiple targets and actions.
  • Compiler is unable to check that the receiver can handle the selector it provides as an action. This often leads to crashes at runtime after inattentive code refactoring.
  • NSInvocation doesn’t automatically nullify reference to the target which got deallocated, so the app can crash at the runtime when the callback is triggered. Thus every target should explicitly remove itself from target-action.
  • For the client code it is not clear which method signature should the action have (how many parameters and of which type). Passing around arguments is quite error-prone. The same applies to the returned type. Static code analyzer won’t be able to warn of any type discrepancies.
  • Shares with delegate the problem of distributed business logic (low cohesion) and promotion of the massive view controller.

Responder chain

Although responder chain is the core mechanism in UIKit originally designed for distribution of touch and motion input events for the views and view controllers, it certainly can be used as a callback technique and in some cases is a great alternative to the delegation.

On its core responder chain is a beautiful application of the chain of responsibility design pattern. The central role in the responder chain is allocated to the UIResponder class.

In Cocoa pretty much every class related to the UI is a direct or indirect descendant of the UIResponder class, thus the responder chain is truly pervasive in the app and includes every view on the screen, their view controllers, and even the UIApplication itself.

A great feature of the responder chain is that it is maintained automatically unless the developer wants to implement a custom UIResponder (which I cannot foresee when might be needed).

So, suppose you have this nontrivial view hierarchy:

UIApplicationUIWindowViewController AViewControllerViewSubview B

The responder chain allows the Subview B send a message to the ViewController A with ease. The ViewController A should just be the first in the chain who implements the selector that Subview B is sending. And if no one including UIApplication can handle the selector - it just gets discarded and the app does not crash.

Not every UIResponder can send a message in the pipe - in Cocoa, there are only two classes which can do it: UIControl and UIApplication, and UIControl in fact uses UIApplication’s API for that.

It’s possible to send actions on behalf of UIControl

1
2
3
4
let control = UIButton()
// UIControl should be added to the view hierarchy
view.addSubview(control)
control.sendAction(#selector(handleAction(sender:)), to: nil, for: nil)

… but normally you’d use UIApplication

1
2
3
// Triggering callback with UIApplication

UIApplication.shared.sendAction(#selector(handleAction(sender:)), to: nil, from: self, for: nil)

As written in the docs, if we provided non-nil UIResponder object in the from parameter (this could be our current view or view controller), the responder chain would start traversing from this element, otherwise, when the from parameter is nil, the traverse starts at the first responder.

For the needs of calling back through a few layers of nested child view controllers, we should always specify the from parameter, because first responder does not necessarily belong to our subviews hierarchy.

Advantages of using Responder chain

  • Supported by every UIView and UIViewController in the project out of the box.
  • Responder chain is managed automatically as the view hierarchy changes.
  • Natively supported by the Interface Builder - without writing code at all you can choose to deliver action from a specific UIControl to the first responder in the chain.
  • Action delivery is crash-safe. If there were no responder that agreed to step in and handle the action - nothing bad happens.
  • Sometimes this is the easiest way to hear back from a view or view controller residing deeply in the view hierarchy. Normally you’d need to forward the callback through all the view controllers manually with a chain of delegates, and this implies writing custom classes for each UIViewController or UIView on the way. This technique truly saves us from writing a lot of code.

Disadvantages of using Responder chain

  • Responder chain is for UIResponder descendants only. Objects of arbitrary classes are not supported.
  • You cannot send any useful data with the action. The parameters for the action are predefined as sender: Any and event: UIEvent?.
  • It is impossible to get a non-void result of the call
  • Responder chain is a simplex channel and you cannot choose the direction.
  • Just like any binding from Interface Builder, the connection may break as you refactor the code. There are some tools that can help with detecting the incorrect state.
  • It is quite confusing for the newcomers why they have to use obscure responder chain for such simple thing as showing the on-screen keyboard in iOS

To be continued…


Be safe, brush your teeth, and write clean code.


P.S. Add me on LinkedIn or follow on Twitter to be notified about the coming posts about practical hints and tricks in iOS engineering!