RACSignal 和一系列视图模型,有没有更被动的方式?
RACSignal and an array of view models, is there a more reactive way?
我使用 UITableViewController
来显示发现的设备。它们是从 SDDeviceBrowser
获得的,它不断地扫描它们并在每次找到新设备时调用它的委托方法。我创建了一个 RACSignal
这样的:
@implementation SDDeviceBrowser (RAC)
- (RACSignal*)rac_newDeviceSignal {
return [self rac_signalForSelector:@selector(deviceBrowserDidFindNewDevice:) fromProtocol:@protocol(SDDeviceBrowserDelegate)];
}
@end
我使用 MVVM 模式:我的 table 视图的视图模型是 DeviceListViewModel
。它有一个数组 devices
,包含绑定到 table 视图单元的子视图模型。它映射浏览器的信号并将其公开给视图控制器:
@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;
}];
}
然后 table 视图控制器订阅 deviceFoundSignal
并在发现新设备时插入一行:
[[self.viewModel.deviceFoundSignal throttle:0.5] subscribeNext:^(id value) {
[self.refreshControl endRefreshing];
//insert new rows to the table view inside beginUpdates/endUpdates
}];
也可以 "reset" 设备浏览器:它会清除已发现设备的列表并重新开始扫描。但是,我找不到一个很好的响应式解决方案来处理这个问题——我只是执行以下操作(在视图控制器中):
[[self.refreshControl rac_signalForControlEvents:UIControlEventValueChanged] subscribeNext:^(id x) {
[self.viewModel restartScanning]; //clears the 'devices' array and restarts the browser
[self.tableView reloadData];
}];
这可行,但我认为可以用更 "reactive" 的方式完成。将新视图模型添加到 map:
块内的数组看起来有点难看。我是否遗漏了可以在此处使用的 ReactiveCocoa 的任何功能?
要组装设备阵列,请查看 -scanWithStart:reduce:
。使用这种方法,您可以从一个空数组开始,然后让 reduce 块将每个设备添加到数组中。例如:
[[[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];
}]
这对 "reset" 功能没有多大作用。要将 "adding" 和 "reseting" 合并为一个信号和一次扫描,我将执行以下操作:
首先,在您的代码中找到可以公开两个相关信号的某个位置,即 -rac_newDeviceSignal
以及哪个信号代表导致重置的事件。我将后一个信号称为“resetSignal
”。
有了设备添加信号和重置信号,我会将它们分别映射到设备阵列上的 "operations"。我所说的 "operation" 是什么意思,基本上是一个采用旧设备阵列的块,以及 returns 一个新设备阵列。
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 @[];
};
}]
有了这两个信号,它们可以 +merge:
变成一个信号,然后可以像上面显示的那样进行扫描。
[[RACSignal
merge:@[ addOperation, resetOperation ]]
scanWithStart:@[] reduce:(NSArray *devices, NSArray *(^operation)(NSArray *)) {
return operation(devices);
}]
我使用 UITableViewController
来显示发现的设备。它们是从 SDDeviceBrowser
获得的,它不断地扫描它们并在每次找到新设备时调用它的委托方法。我创建了一个 RACSignal
这样的:
@implementation SDDeviceBrowser (RAC)
- (RACSignal*)rac_newDeviceSignal {
return [self rac_signalForSelector:@selector(deviceBrowserDidFindNewDevice:) fromProtocol:@protocol(SDDeviceBrowserDelegate)];
}
@end
我使用 MVVM 模式:我的 table 视图的视图模型是 DeviceListViewModel
。它有一个数组 devices
,包含绑定到 table 视图单元的子视图模型。它映射浏览器的信号并将其公开给视图控制器:
@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;
}];
}
然后 table 视图控制器订阅 deviceFoundSignal
并在发现新设备时插入一行:
[[self.viewModel.deviceFoundSignal throttle:0.5] subscribeNext:^(id value) {
[self.refreshControl endRefreshing];
//insert new rows to the table view inside beginUpdates/endUpdates
}];
也可以 "reset" 设备浏览器:它会清除已发现设备的列表并重新开始扫描。但是,我找不到一个很好的响应式解决方案来处理这个问题——我只是执行以下操作(在视图控制器中):
[[self.refreshControl rac_signalForControlEvents:UIControlEventValueChanged] subscribeNext:^(id x) {
[self.viewModel restartScanning]; //clears the 'devices' array and restarts the browser
[self.tableView reloadData];
}];
这可行,但我认为可以用更 "reactive" 的方式完成。将新视图模型添加到 map:
块内的数组看起来有点难看。我是否遗漏了可以在此处使用的 ReactiveCocoa 的任何功能?
要组装设备阵列,请查看 -scanWithStart:reduce:
。使用这种方法,您可以从一个空数组开始,然后让 reduce 块将每个设备添加到数组中。例如:
[[[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];
}]
这对 "reset" 功能没有多大作用。要将 "adding" 和 "reseting" 合并为一个信号和一次扫描,我将执行以下操作:
首先,在您的代码中找到可以公开两个相关信号的某个位置,即 -rac_newDeviceSignal
以及哪个信号代表导致重置的事件。我将后一个信号称为“resetSignal
”。
有了设备添加信号和重置信号,我会将它们分别映射到设备阵列上的 "operations"。我所说的 "operation" 是什么意思,基本上是一个采用旧设备阵列的块,以及 returns 一个新设备阵列。
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 @[];
};
}]
有了这两个信号,它们可以 +merge:
变成一个信号,然后可以像上面显示的那样进行扫描。
[[RACSignal
merge:@[ addOperation, resetOperation ]]
scanWithStart:@[] reduce:(NSArray *devices, NSArray *(^operation)(NSArray *)) {
return operation(devices);
}]