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?
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);
}]