Yue Liu Yue Liu - 3 months ago 18
iOS Question

With MVVM and ReactiveCocoa, how to handle the delegate pattern in iOS?

A common situation is to have a View Controller

A
, and it has some information which will be sent to View Controller
B
; and
B
will edit the information, when
B
finishes editing the information,
B
will call the delegate method to update
A
, and pop itself from the navigation controller.

How to handle this problem with MVVM and ReactiveCocoa?

Answer

Heavy use of ReactiveCocoa, in general, will start pushing you away from the delegate pattern. However, since much of the code you've already written and all of the code you'll encounter in the iOS standard libraries use it, being able to interact with it is still important.

You'll want to use the -[NSObject rac_signalForSelector:] category, that will return a signal that receives a RACTuple value of the arguments to a method each time it is invoked, and completes when the object sending the signal is deallocated.

Let's say you have a UIViewController to display that contains a list of checkboxes a user can select, with a continue button at the bottom. Since the selections change over time, you could represent it as an RACSignal of NSIndexSet values. For the purposes of this example, let's say you must use this class as is, and it currently declares a delegate pattern that contains the following:

@class BSSelectionListViewController;
@protocol BSSelectionListViewControllerDelegate <NSObject>
   - (void)listChangedSelections:(BSSelectionListViewController*)list;
   - (void)listContinueTouched:(BSSelectionListViewController*)list;
@end

When you present the view controller from elsewhere (like a UIViewController at the top of the navigation stack), you'll create the view controller and assign self as the delegate. It might look something like

BSSelectionListViewController* listVC = [[BSSelectionListViewController alloc] initWithQuestion:question listChoices:choices selections:idxSet];
    listVC.delegate = self;
[self.navigationController pushViewController:listVC];

Before pushing this UIViewController on the stack, you'll want to create signals for the delegate methods that it could call:

RACSignal* continueTouched = [[[self rac_signalForSelector:@selector(listContinueTouched:)]
                                  takeUntil:list.rac_willDeallocSignal]
                                  filter:^BOOL(RACTuple* vcTuple)
    {
        return vcTuple.first == listVC;
    }];

    RACSignal* selections = [[[[self rac_signalForSelector:@selector(listChangedSelections:)]
                              takeUntil:list.rac_willDeallocSignal]
                              filter:^BOOL(RACTuple* vcTuple)
    {
        return vcTuple.first == listVC;
    }]
                             map:^id(RACTuple* vcTuple)
    {
        return [vcTuple.first selections];
    }];

You can then subscribe to these signals to do whatever side effects you need. Maybe something like:

RAC(self, firstChoiceSelected) = [selections map:^id(NSIndexSet* selections)
    {
        return @([selections containsIndex:0]);
    }];

and

@weakify(self)
[continueTouched subscribeNext:^(id x)
{
    @strongify(self)
    [self.navigationController popToViewController:self];
}];

Because it's possible that you might have several of these screens that you're the delegate of, you want to make sure that you are filtering down to just this one in your RACSignals.

ReactiveCocoa will actually implement these methods (the ones in the delegate protocol) for you. However, to keep the compiler happy, you should add stubs.

- (void)listChangedSelections:(BSSelectionListViewController *)list {}
- (void)listContinueTouched:(BSSelectionListViewController*)list {}

This is, IMO, an improvement over the standard delegate pattern, where you would need to declare an instance variable to hold the selection view controller, and check in the delegate methods which controller is calling you. ReactiveCocoa's rac_signalForSelector method can reduce the scope of that state (this view controller comes and goes over time) in to a local variable instead of an instance variable. It also allows you to be explicit about dealing with the changes to the selections.