28

我正在努力将 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];
    }
}];

视图模型.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;

视图模型.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 提供tableView: cellForRowAtIndexPath:. 我是否只需要以“传统”方式进行这些操作,还是可以为细胞提供某种信号提供者?或者最好让它保持原样,因为 ViewModel 不应该真正关心构建视图,而只是修改视图的源?

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

任何其他评论也将不胜感激。这项工作的目标是制作一个预取/缓存 ViewModel 层,可以在需要在后台加载数据时发出信号,从而减少设备上的等待时间。如果由此产生任何可重用的东西(除了模式),它当然是开源的。

编辑:还有另一个问题:根据文档,我应该使用 ViewModel 中所有信号的属性而不是方法?我想我应该在init中配置它们?或者我应该保持原样以便吸气剂返回新信号?

我应该active在 ReactiveCocoa 的 github 帐户中拥有 ViewModel 示例中的属性吗?

4

2 回答 2

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)更好的方法?

考虑使用 a RACCommandhere 代替。它将为您提供一种更好的方式来处理并发请求、错误和线程安全。命令是从视图到视图模型的一种非常典型的通信方式。

我是否应该像 ReactiveCocoa 的 github 帐户中的 ViewModel 示例那样具有活动属性?

这仅取决于您是否需要它。在 iOS 上,它可能不像 OS X 那样普遍需要,在 OS X 中,您可以分配多个视图和视图模型,但不能一次“激活”。

希望这对您有所帮助。看起来您通常朝着正确的方向前进!

于 2013-07-09T17:54:46.913 回答
4

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

正如上面 joshaber所描述的,标准方法是在视图控制器中手动实现数据源和委托,视图模型只是公开一个项目数组,每个项目代表一个支持表格视图单元格的视图模型。

但是,这会导致在您原本优雅的视图控制器中出现很多样板。

我创建了一个简单的绑定助手,它允许您将视图模型的 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];

它还处理选择,在选择行时执行命令。完整的代码在我的博客上。希望这可以帮助!

于 2014-05-13T05:11:49.530 回答