私は、ネットワークからの簡単なキャッシュ/プリフェッチ (および MVVM の他のすべての利点) を可能にする ViewModel レイヤーを作成することを目標に、私のプロジェクトに RAC を統合することに取り組んでいます。私はまだ MVVM や FRP に特に精通していませんが、iOS 開発用の再利用可能な素敵なパターンを開発しようとしています。これについていくつか質問があります。
まず、これは、ViewModel をビューの 1 つに追加した方法のようなものです。試してみるためです。(これは後で参照したいと思います)。
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 の場合、delegate と dataSource の両方を提供する必要があります。コントローラーにプロパティを設定し、NSUInteger numberOfRowsInTable
それを ViewModel のシグナルにバインドする必要がありますか? また、RAC を使用して TableView にセルを提供する方法がよくわかりませんtableView: cellForRowAtIndexPath:
。これらを「従来の」方法で行う必要があるだけですか、それとも細胞に何らかのシグナルプロバイダーを用意することは可能ですか? それとも、ViewModel はビューのソースを変更するだけで、ビューの構築に実際に関与するべきではないため、そのままにしておくのが最善でしょうか?
さらに、サブジェクト (fetchDoctorSubject) の使用よりも優れたアプローチはありますか?
他のコメントも同様に高く評価されます。この作業の目標は、バックグラウンドでデータをロードする必要があるときにいつでも通知できるプリフェッチ/キャッシュ ビューモデル レイヤーを作成し、デバイスでの待機時間を短縮することです。これから再利用可能なもの (パターン以外) が出てくる場合、それはもちろんオープンソースになります。
編集:そして別の質問:ドキュメントによると、メソッドの代わりにViewModelのすべてのシグナルにプロパティを使用する必要があるようです? initでそれらを設定する必要があると思いますか?または、ゲッターが新しいシグナルを返すようにそのままにしておく必要がありますか?
active
ReactiveCocoa の github アカウントにある ViewModel の例のようなプロパティが必要ですか?