2013-07-09 22 views
28

我正在将RAC集成到我的项目中,目标是创建一个ViewModel层,以便从网络轻松缓存/预取(以及MVVM的所有其他优点)。我对MVVM或FRP还不是特别熟悉,我正在尝试为iOS开发开发一个很好的,可重用的模式。我有几个关于这个问题。用于具有ReactiveCocoa的iOS应用程序的ViewModel模式

首先,这是我如何添加ViewModel到我的一个视图,只是为了尝试一下。 (我想在这里稍后参考)。

在视图控制器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中配置它们?或者我应该保持原样,以便吸气剂返回新的信号?

我应该有一个active属性,就像ReactiveCocoa的github账户中的ViewModel示例一样吗?

回答

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:中的单元格。我只需要以“传统”的方式来做这些事情,还是有可能为这些单元提供某种信号提供者?或者最好让它保持原样,因为ViewModel不应该真的关心构建视图,只需修改视图的来源?

最后一行是完全正确的。你的视图模型应该给视图控制器显示数据(数组,集合,不管),但你的视图控制器仍然是表视图的委托和数据源。视图控制器创建单元格,但单元格由视图模型中的数据填充。如果您的细胞相对复杂,您甚至可以拥有细胞观察模型。

此外,有没有比我使用的主题(fetchDoctorSubject)更好的方法?

请考虑在此处使用RACCommand代替。它会给你一个处理并发请求,错误和线程安全的更好方法。命令是从视图到视图模型的一种非常典型的通信方式。

我应该像ReactiveCocoa的github帐户中的ViewModel示例那样拥有一个活动属性吗?

这取决于你是否需要它。在iOS上,它可能比OS X更少需要,OS X可以分配多个视图和视图模型,但不能同时处于“活动”状态。

希望这对我有帮助。它看起来像你正朝着正确的方向前进!

+1

这非常有帮助,谢谢!我认为RACCommand是我想要的,而您的其他意见已经解决了其他几个问题。 –

+1

Josh,感谢你写这篇文章,它总是看到这些真实世界的例子被解构。 – cbowns

4

对于一个UITableView,例如,我们需要同时提供委托和 数据源。我应该在控制器NSUInteger numberOfRowsInTable上拥有一个属性并将其绑定到ViewModel上的信号?

的标准方法,如通过joshaber above描述是手动执行视图控制器内的数据源和委托,与视图模型简单地暴露项的阵列的每一个表示它备份表视图细胞视图模型。

但是,这会导致批次锅炉板在您的其他优雅的视图控制器。

我创建了一个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。希望这可以帮助!

相关问题