gpt4 book ai didi

ios - 使用 ReactiveCocoa 的 iOS 应用程序的 ViewModel 模式

转载 作者:IT王子 更新时间:2023-10-29 07:51:37 26 4
gpt4 key购买 nike

我正在努力将 RAC 集成到我的项目中,目标是创建一个 ViewModel 层,该层将允许从网络轻松缓存/预取(以及 MVVM 的所有其他好处)。我还不是特别熟悉 MVVM 或 FRP,我正在尝试为 iOS 开发开发一个不错的、可重用的模式。我对此有几个问题。

首先,这就是我将 ViewModel 添加到我的一个 View 的方式,只是为了尝试一下。 (我想在这里稍后引用)。

在 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 直接绑定(bind)到 textView 的隐藏属性是否有意义?

我的主要问题是将委托(delegate)处理的问题转移到 ViewModel 中(希望如此)。委托(delegate)在 iOS 世界中非常普遍,我想找出最好的解决方案,甚至只是适度可行的解决方案。

例如,对于 UITableView,我们需要同时提供委托(delegate)和数据源。我是否应该在 Controller NSUInteger numberOfRowsInTable 上设置一个属性并将其绑定(bind)到 ViewModel 上的信号?我真的不清楚如何使用 RAC 为我的 TableView 提供 tableView: cellForRowAtIndexPath: 中的单元格。我是否只需要以“传统”方式执行这些操作,或者是否可以为细胞提供某种信号提供者?或者也许最好让它保持原样,因为 ViewModel 不应该真正关心构建 View ,而只需修改 View 的源代码?

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

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

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

我是否应该像 ReactiveCocoa 的 github 帐户中的 ViewModel 示例那样拥有一个 active 属性?

最佳答案

View 模型应该为 View 建模。也就是说,它不应该规定任何 View 外观本身,而是规定 View 外观背后的逻辑。它不应该直接了解 View 。这是一般指导原则。

关于一些细节。

It looks like according to the documentation, I should be using properties for all of the signals in my ViewModel instead of methods? I think I should configure them in init? Or should I leave it as-is so that getters return new signals?

是的,我们通常只使用反射(reflect)其模型属性的属性。我们会在 -init 中配置它们,有点像:

- (id)init {
self = [super init];
if (self == nil) return nil;

RAC(self.title) = RACAbleWithStart(self.model.title);

return self;
}

请记住, View 模型只是用于特定用途的模型。具有普通旧属性的普通旧对象。

Am I right in the way I'm using signals? Specifically, does it make sense to have bioSignal to update the data as well as a hiddenBioSignal to directly bind to the hidden property of a textView?

如果生物信号的隐藏是由某些特定的模型逻辑驱动的,那么将其作为 View 模型的属性公开是有意义的。但尽量不要从隐藏性等观点的角度来考虑它。也许更多的是关于有效性、加载等。一些与具体呈现方式无关的东西。

For a UITableView, for example, we need to provide both a delegate and a dataSource. Should I have a property on my controller NSUInteger numberOfRowsInTable and bind it to a signal on the ViewModel? And I'm really unclear on how to use RAC to provide my TableView with cells in tableView: cellForRowAtIndexPath:. Do I just need to do these the "traditional" way or is it possible to have some sort of signal provider for the cells? Or maybe it's best to leave it how it is, because a ViewModel shouldn't really be concerned with building the views, just modifying the source of the views?

最后一行完全正确。您的 View 模型应该为 View Controller 提供要显示的数据(数组、集合等),但您的 View Controller 仍然是 TableView 的委托(delegate)和数据源。 View Controller 创建单元格,但单元格由 View 模型中的数据填充。如果您的单元格相对复杂,您甚至可以拥有单元格 View 模型。

Further, is there a better approach than my use of a subject (fetchDoctorSubject)?

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

Should I have an active property as in the ViewModel example in ReactiveCocoa's github account?

就看你是否需要了。在 iOS 上,它可能不像 OS X 那样普遍需要,在 OS X 上,您可以分配多个 View 和 View 模型,但不能同时“激活”。

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

关于ios - 使用 ReactiveCocoa 的 iOS 应用程序的 ViewModel 模式,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/17551369/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com