我正在努力将RAC集成到我的项目中,目标是创建一个ViewModel层,允许从网络轻松缓存/预取(以及MVVM的所有其他好处)。我对MVVM或FRP还不是特别熟悉,我正在尝试为iOS开发开发一个漂亮的,可重用的模式。我对此有几个问题。
首先,这就是我将ViewModel添加到我的一个视图中的方式,只是为了尝试一下。 (我想在这里稍后参考)。
在ViewController viewDidLoad中:
@weakify(self)
//Setup signals
RAC(self.navigationItem.title) = self.viewModel.nameSignal;
RAC(self.specialtyLabel.text) = self.viewModel.specialtySignal;
RAC(self.bioButton.hidden) = self.viewModel.hiddenBioSignal;
RAC(self.bioTextView.text) = self.viewModel.bioSignal;
RAC(self.profileImageView.hidden) = self.viewModel.hiddenProfileImageSignal;
[self.profileImageView rac_liftSelector:@selector(setImageWithContentsOfURL:placeholderImage:) withObjectsFromArray:@[self.viewModel.profileImageSignal, [RACTupleNil tupleNil]]];
[self.viewModel.hasOfficesSignal subscribeNext:^(NSArray *offices) {
self.callActionSheet = [[UIActionSheet alloc] initWithTitle:@"Choose Office" delegate:self cancelButtonTitle:nil destructiveButtonTitle:nil otherButtonTitles:nil];
self.directionsActionSheet = [[UIActionSheet alloc] initWithTitle:@"Choose Office" delegate:self cancelButtonTitle:nil destructiveButtonTitle:nil otherButtonTitles:nil];
self.callActionSheet.delegate = self;
self.directionsActionSheet.delegate = self;
}];
[self.viewModel.officesSignal subscribeNext:^(NSArray *offices){
@strongify(self)
for (LMOffice *office in offices) {
[self.callActionSheet addButtonWithTitle: office.name ? office.name : office.address1];
[self.directionsActionSheet addButtonWithTitle: office.name ? office.name : office.address1];
//add offices to maps
CLLocationCoordinate2D coordinate = {office.latitude.doubleValue, office.longitude.doubleValue};
MKPointAnnotation *point = [[MKPointAnnotation alloc] init];
point.coordinate = coordinate;
[self.mapView addAnnotation:point];
}
//zoom to include all offices
MKMapRect zoomRect = MKMapRectNull;
for (id <MKAnnotation> annotation in self.mapView.annotations)
{
MKMapPoint annotationPoint = MKMapPointForCoordinate(annotation.coordinate);
MKMapRect pointRect = MKMapRectMake(annotationPoint.x, annotationPoint.y, 0.2, 0.2);
zoomRect = MKMapRectUnion(zoomRect, pointRect);
}
[self.mapView setVisibleMapRect:zoomRect animated:YES];
}];
[self.viewModel.openingsSignal subscribeNext:^(NSArray *openings) {
@strongify(self)
if (openings && openings.count > 0) {
[self.openingsTable reloadData];
}
}];
ViewModel.h
@property (nonatomic, strong) LMProvider *doctor;
@property (nonatomic, strong) RACSubject *fetchDoctorSubject;
- (RACSignal *)nameSignal;
- (RACSignal *)specialtySignal;
- (RACSignal *)bioSignal;
- (RACSignal *)profileImageSignal;
- (RACSignal *)openingsSignal;
- (RACSignal *)officesSignal;
- (RACSignal *)hiddenBioSignal;
- (RACSignal *)hiddenProfileImageSignal;
- (RACSignal *)hasOfficesSignal;
ViewModel.m
- (id)init {
self = [super init];
if (self) {
_fetchDoctorSubject = [RACSubject subject];
//fetch doctor details when signalled
@weakify(self)
[self.fetchDoctorSubject subscribeNext:^(id shouldFetch) {
@strongify(self)
if ([shouldFetch boolValue]) {
[self.doctor fetchWithCompletion:^(NSError *error){
if (error) {
//TODO: display error message
NSLog(@"Error fetching single doctor info: %@", error);
}
}];
}
}];
}
return self;
}
- (RACSignal *)nameSignal {
return [RACAbleWithStart(self.doctor.displayName) distinctUntilChanged];
}
- (RACSignal *)specialtySignal {
return [RACAbleWithStart(self.doctor.primarySpecialty.name) distinctUntilChanged];
}
- (RACSignal *)bioSignal {
return [RACAbleWithStart(self.doctor.bio) distinctUntilChanged];
}
- (RACSignal *)profileImageSignal {
return [[[RACAbleWithStart(self.doctor.profilePhotoURL) distinctUntilChanged]
map:^id(NSURL *url){
if (url && ![url.absoluteString hasPrefix:@"https:"]) {
url = [NSURL URLWithString:[NSString stringWithFormat:@"https:%@", url.absoluteString]];
}
return url;
}]
filter:^BOOL(NSURL *url){
return (url != nil && ![url.absoluteString isEqualToString:@""]);
}];
}
- (RACSignal *)openingsSignal {
return [RACAbleWithStart(self.doctor.openings) distinctUntilChanged];
}
- (RACSignal *)officesSignal {
return [RACAbleWithStart(self.doctor.offices) distinctUntilChanged];
}
- (RACSignal *)hiddenBioSignal {
return [[self bioSignal] map:^id(NSString *bioString) {
return @(bioString == nil || [bioString isEqualToString:@""]);
}];
}
- (RACSignal *)hiddenProfileImageSignal {
return [[self profileImageSignal] map:^id(NSURL *url) {
return @(url == nil || [url.absoluteString isEqualToString:@""]);
}];
}
- (RACSignal *)hasOfficesSignal {
return [[self officesSignal] map:^id(NSArray *array) {
return @(array.count > 0);
}];
}
我是否正确使用信号?具体来说,让bioSignal
更新数据以及hiddenBioSignal
直接绑定到textView的隐藏属性是否有意义?
我的主要问题伴随着移动的问题,这些问题已经由代理人处理到ViewModel中(希望如此)。代表们在iOS世界中非常普遍,我想找出最好的,甚至只是一个适度可行的解决方案。
例如,对于UITableView,我们需要同时提供委托和数据源。我应该在控制器NSUInteger numberOfRowsInTable
上有一个属性并将其绑定到ViewModel上的信号吗?我真的不清楚如何使用RAC在tableView: cellForRowAtIndexPath:
中为我的TableView提供单元格。我只需要以“传统”的方式进行这些操作,还是可以为细胞提供某种信号提供者?或者也许最好留下它是怎么回事,因为ViewModel不应该真的关心构建视图,只是修改视图的来源?
此外,有没有比使用主题更好的方法(fetchDoctorSubject)?
任何其他评论也将受到赞赏。这项工作的目标是制作一个预取/缓存ViewModel层,可以在需要时在后台加载数据时发出信号,从而减少设备上的等待时间。如果有任何可重复使用的东西(除了模式之外),它当然是开源的。
编辑:还有一个问题:看起来根据文档,我应该使用ViewModel中所有信号的属性而不是方法?我想我应该在init中配置它们?或者我应该保持原样让吸气剂返回新信号?
我是否应该在ReactiveCocoa的github帐户中的ViewModel示例中拥有active
属性?
答案 0 :(得分:36)
视图模型应该为视图建模。也就是说,它不应该决定任何视图外观本身,而是视图外观背后的逻辑。它不应该直接了解视图。这是一般指导原则。
关于某些细节。
看起来根据文档,我应该使用ViewModel中所有信号的属性而不是方法?我想我应该在init中配置它们?或者我应该保持原样让吸气剂返回新信号?
是的,我们通常只使用镜像其模型属性的属性。我们在-init
中配置它们有点像:
- (id)init {
self = [super init];
if (self == nil) return nil;
RAC(self.title) = RACAbleWithStart(self.model.title);
return self;
}
请记住,视图模型只是特定用途的模型。具有普通旧属性的普通旧对象。
我是否正确使用信号?具体来说,让
bioSignal
更新数据以及hiddenBioSignal
直接绑定到textView的隐藏属性是否有意义?
如果生物信号的隐藏性是由某些特定的模型逻辑驱动的,那么将它作为视图模型上的属性公开是有意义的。但是,尽量不要将其视为隐藏的视角。也许它更多的是关于有效性,加载等等。某些东西与它的具体表现无关。
例如,对于UITableView,我们需要同时提供委托和数据源。我应该在我的控制器NSUInteger numberOfRowsInTable上有一个属性并将其绑定到ViewModel上的信号吗?而且我真的不清楚如何使用RAC在tableView中为我的TableView提供单元格:cellForRowAtIndexPath:。我只需要做这些传统的&#34;方式或是否有可能为细胞提供某种信号提供者?或者最好留下它的样子,因为ViewModel不应该真的关心构建视图,只是修改视图的来源?
最后一行是完全正确的。您的视图模型应该为视图控制器提供要显示的数据(数组,集合等),但您的视图控制器仍然是表视图的委托和数据源。视图控制器创建单元格,但单元格由视图模型中的数据填充。如果您的单元格相对复杂,您甚至可以拥有单元格视图模型。
此外,有没有比使用主题更好的方法(fetchDoctorSubject)?
请考虑在此使用RACCommand
。它将为您提供处理并发请求,错误和线程安全的更好方法。命令是从视图到视图模型进行通信的一种非常典型的方式。
我是否应该像ReactiveCocoa的github帐户中的ViewModel示例中那样拥有活动属性?
这取决于你是否需要它。在iOS上,它可能不太常见于OS X,在那里你可以有多个视图和视图模型已分配,但不是&#34;活动&#34;马上。
希望这会有所帮助。看起来你一般都朝着正确的方向前进!
答案 1 :(得分:4)
例如,对于UITableView,我们需要同时提供委托和 一个dataSource。我应该在我的控制器NSUInteger上有一个属性 numberOfRowsInTable并将其绑定到ViewModel上的信号?
joshaber above描述的标准方法是在视图控制器中手动实现数据源和委托,视图模型只是公开一个项目数组,每个项目代表一个支持表格视图的视图模型细胞
然而,这会在您优雅的视图控制器中产生 lot 样板。
我创建了一个simple binding helper,它允许您使用几行代码将NSArray视图模型绑定到表视图:
// create a cell template
UINib *nib = [UINib nibWithNibName:@"CETweetTableViewCell" bundle:nil];
// bind the ViewModels 'searchResults' property to a table view
[CETableViewBindingHelper bindingHelperForTableView:self.searchResultsTable
sourceSignal:RACObserve(self.viewModel, searchResults)
templateCell:nib];
它还处理选择,在选择行时执行命令。完整的代码是over on my blog。希望这有帮助!