Search code examples
iosobjective-cmvvmreactive-cocoa

RACSignal and an array of view models, is there a more reactive way?


I use UITableViewController to show discovered devices. They are obtained from SDDeviceBrowser, which scans for them constantly and calls its delegate method each time a new device is found. I created a RACSignal like this:

@implementation SDDeviceBrowser (RAC)
- (RACSignal*)rac_newDeviceSignal {
    return [self rac_signalForSelector:@selector(deviceBrowserDidFindNewDevice:) fromProtocol:@protocol(SDDeviceBrowserDelegate)];
}
@end

I use MVVM pattern: my table view's view model is DeviceListViewModel. It has an array devices, containing child view models which are bound to table view cells. It maps the browser's signal and exposes it for the view controller:

@interface DeviceListViewModel ()
@property (strong, readwrite, nonatomic) NSArray *devices;
@property (strong, nonatomic) SDDeviceBrowser *browser;
@end

//boring initialization ommitted

- (RACSignal *)deviceFoundSignal {
    return [[self.browser rac_newDeviceSignal] map:^id(RACTuple* parameters) {
      SDDevice *device = parameters.last;
      DeviceViewModel *deviceViewModel = [[DeviceViewModel alloc] initWithDevice:device];
      self.devices = [self.devices arrayByAddingObject:deviceViewModel];
      return deviceViewModel;
    }];
}

Then the table view controller subscribes to deviceFoundSignal and inserts a row whenever a new device is found:

[[self.viewModel.deviceFoundSignal throttle:0.5] subscribeNext:^(id value) {
  [self.refreshControl endRefreshing];
  //insert new rows to the table view inside beginUpdates/endUpdates 
}];

It is also possible to "reset" the device browser: it clears the list of discovered devices and starts scanning again. However, I couldn't find a nice reactive solution to handle that - I just do the following (in the view controller):

[[self.refreshControl rac_signalForControlEvents:UIControlEventValueChanged] subscribeNext:^(id x) {
  [self.viewModel restartScanning];  //clears the 'devices' array and restarts the browser
  [self.tableView reloadData];
}];

This works, but I think it could be done in a more "reactive" way. Adding new view model to the array inside the map: block looks kinda ugly. Am I missing any features of ReactiveCocoa that can be used here?


Solution

  • For assembling the array of devices, check out -scanWithStart:reduce:. With this method, you could start with an empty array, and have the reduce block add each device to the array. For example:

    [[[self.browser
        rac_newDeviceSignal]
        map:^(RACTuple *parameters) {
            SDDevice *device = parameters.last;
            return [[DeviceViewModel alloc] initWithDevice:device];
        }]
        scanWithStart:@[] reduce:^(NSArray models, DeviceViewModel *deviceViewModel) {
            return [devices arrayByAddingObject:deviceViewModel];
        }]
    

    This doesn't do much for the "reset" functionality. To combine "adding" and "reseting" into one signal and one scan, I would do the following:

    First, find somewhere in your code that you can expose the two pertinent signals, namely -rac_newDeviceSignal and which ever signal represents the events that causes a reset. I'll call this latter signal "resetSignal".

    With a signal of device additions, and a signal of resets, I would map them each to "operations" on the array of devices. What do I mean by "operation", basically a block that takes the old array of devices, and returns a new array of devices.

    RACSignal *addOperation = [[self.browser rac_newDeviceSignal] map:^(RACTuple *parameters) {
        return ^(NSArray *devices) {
            SDDevice *device = parameters.last;
            DeviceViewModel *model = [[DeviceViewModel alloc] initWithDevice:device];
            return [devices arrayByAddingObject:model];
        };
    }]
    
    RACSignal *resetOperation = [resetSignal map:^(id _) {
        return ^(NSArray *devices) {
            return @[];
        };
    }]
    

    With these two signals in place, they can be +merge:'d into a single signal, which can then be scanned on like shown up above.

    [[RACSignal
        merge:@[ addOperation, resetOperation ]]
        scanWithStart:@[] reduce:(NSArray *devices, NSArray *(^operation)(NSArray *)) {
            return operation(devices);
        }]