28

私は、ネットワークからの簡単なキャッシュ/プリフェッチ (および 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でそれらを設定する必要があると思いますか?または、ゲッターが新しいシグナルを返すようにそのままにしておく必要がありますか?

activeReactiveCocoa の 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 の場合、delegate と dataSource の両方を提供する必要があります。コントローラー NSUInteger numberOfRowsInTable にプロパティを設定し、それを ViewModel のシグナルにバインドする必要がありますか? また、RAC を使用して TableView に tableView: cellForRowAtIndexPath: のセルを提供する方法については、本当によくわかりません。これらを「従来の」方法で行う必要があるだけですか、それとも細胞に何らかのシグナルプロバイダーを用意することは可能ですか? それとも、ViewModel はビューのソースを変更するだけで、ビューの構築に実際に関与するべきではないため、そのままにしておくのが最善でしょうか?

その最後の行はまさに正しいです。ビュー モデルはビュー コントローラーに表示するデータ (配列、セットなど) を与える必要がありますが、ビュー コントローラーは依然としてテーブル ビューのデリゲートおよびデータ ソースです。ビュー コントローラーはセルを作成しますが、セルにはビュー モデルからのデータが入力されます。セルが比較的複雑な場合は、セル ビュー モデルを使用することもできます。

さらに、サブジェクト (fetchDoctorSubject) の使用よりも優れたアプローチはありますか?

RACCommand代わりに hereを使用することを検討してください。これにより、同時リクエスト、エラー、およびスレッド セーフをより適切に処理できます。コマンドは、ビューからビュー モデルに通信するための非常に一般的な方法です。

ReactiveCocoa の github アカウントの ViewModel の例のように、アクティブなプロパティが必要ですか?

それはあなたがそれを必要とするかどうかにかかっています。iOS では、複数のビューとビュー モデルを割り当てることができても、一度に「アクティブ」にすることはできない OS X ほど一般的には必要ありません。

うまくいけば、これは役に立ちました。全体的に正しい方向に向かっているようです!

于 2013-07-09T17:54:46.913 に答える
4

たとえば、UITableView の場合、delegate と dataSource の両方を提供する必要があります。コントローラー 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 に答える