11 Comments

Replacing the Objective-C “Delegate Pattern” with ReactiveCocoa

ReactiveCocoa is a library created by GitHub that brings “Functional Reactive Programming” to Objective-C. It provides a number useful features right out of the gate which gives a developer the power to observe, transform, merge, and filter signals that contain values.

More importantly, it has the power to build on top of and replace existing (and sometimes archaic) patterns used in Objective-C. One of the most common of these patterns is the “delegate pattern.”

The Delegate Pattern

The delegate pattern is a pattern of delegating tasks to an object and potentially informing it of certain actions that are being taken. Here’s a typical example of the delegate pattern found in iOS programming:


- (void)viewDidLoad {
    UISearchBar *searchBar = [[UISearchBar alloc] initWithFrame: CGRectZero];
    self.searchController = [[UISearchDisplayController alloc] initWithSearchBar:self.searchBar contentsController:self];
    self.searchController.delegate = self;
    // Place it in view
    searchBar.delegate = self;
}

- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)text {
    self.searchResults = [self search: text];
}

- (void)searchDisplayControllerDidBeginSearch:(UISearchDisplayController *)searchController {
    self.isSearching = YES;
}

- (void)searchDisplayControllerDidEndSearch:(UISearchDisplayController *)searchController {
    self.isSearching = NO;
}

In this particular example, the object (presumably a UIViewController) is the delegate to both the UISearchBar and UISearchDisplayController. The searchBar:textDidChange: delegate method updates the search results and the UISearchDisplayController delegate methods (searchDisplayControllerDidBeginSearch and searchDisplayControllerDidEndSearch respectively) track whether the search is currently active.

There are a couple of issues with this delegate approach:

  1. There is only ever one object that can subscribe to changes to the UISearchBar and UISearchDisplayController. All actions must go through the delegate.
  2. The act of searching is is scattered across three different callbacks.

The ReactiveCocoa Toolbox

ReactiveCocoa has built-in helpers for creating signals from any selector on an object or protocol.

rac_signalForSelector: and rac_signalForSelector:fromProtocol:

These two helpers will create a signal that is bound to a selector, such that any time that selector is invoked on the object it will send a new value through the signal.

rac_liftSelector:withSignals:

This helper will “lift” a selector into “signal space” where the selector is invoked anytime the corresponding argument signals send a new value. The signals passed to the rac_liftSelector must match the number of arguments expressed by the selector.

Replacing the Delegate Pattern

Now that we have familiarized ourselves with some of the more powerful aspects of ReactiveCocoa, we can replace the delegate pattern example with one that uses signals.

UISearchBar

Instead of having to assign a delegate to the UISearchBar and implement searchBar:textDidChange:, let’s modify the UISearchBar so there is a signal representing changes to the text.

@implementation UISearchBar (RAC)
- (RACSignal *)rac_textSignal {
    self.delegate = self;
    RACSignal *signal = objc_getAssociatedObject(self, _cmd);
    if (signal != nil) return signal;
    
    /* Create signal from selector */
    signal = [[self rac_signalForSelector:@selector(searchBar:textDidChange:) 
                    fromProtocol:@protocol(UISearchBarDelegate)] map:^id(RACTuple *tuple) {
        return tuple.second;
    }];
    
    objc_setAssociatedObject(self, _cmd, signal, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    return signal;
}
@end

A new method had been added to UISearchBar (via Objective-C categories) that creates a signal from the selector searchBar:textDidChange: and maps the signal so that it only ever sends the text.

UISearchDisplayController

In order to know whether the UISearchDisplayController is active or not, it requires that two delegate selectors are subscribed to: searchDisplayControllerDidBeginSearch: and searchDisplayControllerDidEndSearch:

@implementation UISearchDisplayController (RAC)
- (RACSignal *)rac_isActiveSignal {
    self.delegate = self;
    RACSignal *signal = objc_getAssociatedObject(self, _cmd);
    if (signal != nil) return signal;
    
    /* Create two signals and merge them */
    RACSignal *didBeginEditing = [[self rac_signalForSelector:@selector(searchDisplayControllerDidBeginSearch:) 
                                        fromProtocol:@protocol(UISearchDisplayDelegate)] mapReplace:@YES];
    RACSignal *didEndEditing = [[self rac_signalForSelector:@selector(searchDisplayControllerDidEndSearch:) 
                                      fromProtocol:@protocol(UISearchDisplayDelegate)] mapReplace:@NO];
    signal = [RACSignal merge:@[didBeginEditing, didEndEditing]];
    
    
    objc_setAssociatedObject(self, _cmd, signal, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    return signal;
}
@end

The implementation above does the following:

  1. Creates a signal from searchDisplayControllerDidBeginSearch: and maps it to YES.
  2. Creates a signal from searchDisplayControllerDidEndSearch and maps it to NO.
  3. Merges the two signals via RACSignal’s merge method where it sends the most recent signal value.

Bringing it All Together

We can now replace the delegate pattern example above with this:

- (void)viewDidLoad {
  UISearchBar *searchBar = [[UISearchBar alloc] initWithFrame: CGRectZero];
  self.searchController = [[UISearchDisplayController alloc] initWithSearchBar:self.searchBar contentsController:self];
  RAC(self, searchResults) = [self rac_liftSelector:@selector(search:) withSignals:self.searchBar.rac_textSignal, nil];
  RAC(self, searching) = [[self.searchController rac_isActiveSignal] doNext:^(id x) {
      NSLog(@"Searching %@", x);
  }];
}

The search results are updated when the search bar’s text changes, and the search is marked as active when the search display controller is active. Moreover, if another object wanted to subscribe for changes, it wouldn’t have to go through a delegate — it could simply subscribe to the signal.

An example iOS project demonstrating the example’s shown above can be found at my Github accout.