使用ReactiveCocoa的iOS应用程序的ViewModel模式
我正在将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,我们需要提供一个委托和一个dataSource。我应该在我的控制器NSUInteger numberOfRowsInTable有一个属性,并绑定到ViewModel上的信号?我真的不清楚如何使用RAC提供我的TableView与tableView中的单元格:cellForRowAtIndexPath:。我只需要做这些“传统”的方式,还是可能有一些类型的信号提供程序的单元格?或者也许最好离开它是怎么回事,因为ViewModel不应该真正关心构建视图,只是修改视图的来源? 此外,是否有一个比我使用主题(fetchDoctorSubject)更好的方法? 任何其他意见,也将赞赏。这项工作的目标是使预取/缓存ViewModel层,可以在需要时在后台加载数据时发出信号,从而减少设备上的等待时间。如果任何可重用的东西来自这个(除了模式),它当然是开源的。 编辑:另一个问题:它看起来像根据文档,我应该使用属性的所有信号在我的ViewModel而不是方法?我想我应该在init中配置它们?或者我应该离开它,因为getter返回新的信号? 我应该有一个活动属性在ReactiveCocoa的github帐户中的ViewModel示例中?
视图模型应该对视图建模。这就是说,它不应该指示任何视图外观本身,而是任何视图外观背后的逻辑。它不应该直接知道任何关于视图。这是一般的指导原则。
关于一些细节。
是的,我们通常只使用反映其模型属性的属性。我们将其配置在ininit有点像: - (id)init { self = [super init]; if (self == nil) return nil; RAC(self.title) = RACAbleWithStart(self.model.title); return self; } 记住,视图模型只是特定用途的模型。纯朴的旧对象与普通的旧属性。
如果生物信号的隐藏是由一些特定的模型逻辑驱动的,那么将它作为视图模型上的一个属性暴露是有意义的。但是尽量不要把它看作隐藏的术语。也许它更多的有效性,加载等。东西没有绑定到具体如何呈现。
最后一行是完全正确的。您的视图模型应该为视图控制器提供要显示的数据(数组,集合,无论什么),但是视图控制器仍然是表视图的委托和数据源。视图控制器创建单元格,但单元格由视图模型中的数据填充。如果你的单元格比较复杂,你甚至可以有一个单元格视图模型。
考虑在这里使用RACCommand。它会给你一个更好的方式来处理并发请求,错误和线程安全。命令是从视图到视图模型的一种非常典型的通信方式。
它只是取决于你是否需要它。在iOS上,可能不太需要比OS X,您可以有多个视图和视图模型分配但不是“活动”一次。 希望这有帮助。看起来你通常在正确的方向前进! (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |