Wzorzec ViewModel dla aplikacji iOS z ReactiveCocoa

Pracuję nad integracją RAC z moim projektem w celu stworzenia warstwy ViewModel, która pozwoli na łatwe buforowanie/prefetchowanie z sieci (plus wszystkie inne zalety MVVM). Nie jestem jeszcze szczególnie zaznajomiony z MVVM lub FRP i staram się opracować ładny, wielokrotnego użytku wzór dla rozwoju iOS. Mam kilka pytań na ten temat.

Po pierwsze, w ten sposób dodałem ViewModel do jednego z moich widoków, aby go wypróbować. (Chcę, aby to tutaj odniesienie później).

W 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);
    }];
}

Czy mam rację w sposobie, w jaki używam sygnałów? W szczególności, czy ma sens posiadanie bioSignal do aktualizacji danych, a także hiddenBioSignal do bezpośredniego powiązania z ukrytą właściwością textView?

Moje podstawowe pytanie pochodzi z poruszających obaw, które byłyby obsługiwane przez delegatów do ViewModel (miejmy nadzieję). Delegaci są tak powszechni w świecie iOS, że chciałbym znaleźć najlepsze, a nawet umiarkowanie wykonalne rozwiązanie.

Dla UITableView, na przykład, musimy dostarczyć zarówno delegata, jak i źródła danych. Czy powinienem mieć właściwość na kontrolerze NSUInteger numberOfRowsInTable i powiązać ją z sygnałem na ViewModel? I naprawdę Nie wiem, jak używać RAC, aby zapewnić mój TableView z komórkami w tableView: cellForRowAtIndexPath:. Czy po prostu muszę to robić w "tradycyjny" sposób, czy jest możliwe, aby mieć jakiś sygnał dostawca komórek? A może lepiej zostawić to tak, jak jest, ponieważ ViewModel nie powinien tak naprawdę zajmować się budowaniem widoków, tylko modyfikacją źródła widoków?

Dalej, czy jest lepsze podejście niż moje użycie tematu (fetchDoctorSubject)?

Wszelkie inne komentarze będą mile widziane, jak również. Celem tej pracy jest stworzenie warstwy ViewModel prefetching/caching, która może być sygnalizowana w razie potrzeby do załadowania danych w tle, a tym samym zmniejszenie oczekiwania razy na urządzeniu. Jeśli z tego wyjdzie coś wielokrotnego użytku (innego niż wzorzec), będzie to oczywiście open source.

Edit: i jeszcze jedno pytanie: wygląda na to, że zgodnie z dokumentacją powinienem używać właściwości dla wszystkich sygnałów w moim ViewModel zamiast metod? Myślę, że powinienem je skonfigurować w init? A może powinienem zostawić to jako - jest tak, że getters zwracają nowe sygnały?

Czy powinienem mieć właściwość active jak w przykładzie ViewModel w reactivecocoa ' s github konto?

Author: Evan Cordell, 2013-07-09

2 answers

Model widoku powinien modelować widok. Co oznacza, że nie powinno dyktować żadnego wyglądu widoku, ale logika stojąca za tym, czym jest wygląd widoku. Nie powinien wiedzieć nic o widoku bezpośrednio. To główna zasada.

Do pewnych szczegółów.

Wygląda na to, że zgodnie z dokumentacją powinienem używać właściwości dla wszystkich sygnałów w moim ViewModel zamiast metod? Myślę, że powinienem je skonfigurować w init? A może powinienem zostawić jak - jest tak, że getters zwracają nowe sygnały?

Tak, zazwyczaj używamy tylko właściwości, które odzwierciedlają ich właściwości modelu. Skonfigurowalibyśmy je w -init tak jakby:

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

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

    return self;    
}

Pamiętaj, że modele widoku są tylko modelami do określonego zastosowania. Zwykłe stare obiekty o zwykłych starych właściwościach.

Czy mam rację w sposobie, w jaki używam sygnałów? W szczególności, czy ma sens mieć bioSignal, aby zaktualizować dane, jak również hiddenBioSignal, aby bezpośrednio powiązać z ukrytą właściwością textView?

Jeśli Ukryty sygnał biologiczny jest napędzany przez jakąś specyficzną logikę modelu, sensowne byłoby ujawnienie go jako właściwości w modelu widoku. Ale staraj się nie myśleć o tym w kategoriach takich jak ukrycie. Może chodzi raczej o walidację, Ładowanie itp. Coś, co nie jest związane z tym, jak jest przedstawione.

Dla UITableView, na przykład, musimy dostarczyć zarówno delegata, jak i źródła danych. Czy mam właściwość na moim kontrolerze nsuinteger numberOfRowsInTable i powiązać go z sygnałem na ViewModel? I naprawdę nie jestem pewien, jak używać RAC, aby zapewnić mój TableView z komórkami w tableView: cellForRowAtIndexPath:. Czy muszę robić to w "tradycyjny" sposób, czy możliwe jest posiadanie jakiegoś Dostawcy Sygnału dla komórek? A może lepiej zostawić to tak, jak jest, ponieważ ViewModel nie powinien tak naprawdę zajmować się budowaniem widoków, tylko modyfikacją źródła widoków?

Ta ostatnia linijka jest dokładnie słuszna. Twój model widoku powinien dać kontrolerowi widoku dane do wyświetlenia (tablicę, zestaw, cokolwiek), ale kontroler widoku nadal Jest delegatem widoku tabeli i źródłem danych. Kontroler widoku tworzy komórki, ale komórki są wypełniane danymi z modelu widoku. Możesz nawet mieć model widoku komórki, jeśli twoje komórki są stosunkowo złożone.

Dalej, czy jest lepsze podejście niż moje użycie tematu (fetchDoctorSubject)?

Rozważ użycie RACCommand tutaj. To da ci łatwiejszy sposób obsługi jednoczesnych żądań, błędów i bezpieczeństwa wątków. Polecenia są dość typowym sposobem komunikowania się z widoku do modelu widoku.

Czy powinienem mieć aktywną właściwość, jak w przykładzie ViewModel na koncie reactivecocoa na GitHubie?

To zależy od tego, czy tego potrzebujesz. Na iOS jest to prawdopodobnie rzadziej potrzebne niż OS X, gdzie można mieć wiele widoków i modeli widoku przydzielonych, ale nie "aktywnych" jednocześnie.

Miejmy nadzieję, że to był pomocny. Wygląda na to, że zmierzasz w dobrym kierunku!

 36
Author: joshaber,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-07-09 17:54:46

Dla UITableView, na przykład, musimy dostarczyć zarówno delegata, jak i źródło danych. Czy mam nieruchomość na moim kontrolerze NSUInteger numerofrowsintable i powiązać go z sygnałem na ViewModel?

Standardowe podejście, opisane przez joshaber powyżej polega na ręcznym zaimplementowaniu źródła danych i delegowaniu delegatów w kontrolerze widoku, przy czym model widoku po prostu eksponuje tablicę elementów, z których każda reprezentuje model widoku, który wspiera widok tabeli cell.

Jednak powoduje to partię płyty kotłowej w Twoim eleganckim kontrolerze widoku.

Stworzyłem prosty Helper wiążący , który pozwala powiązać NSArray modeli widoku z widokiem tabeli za pomocą kilku linijek kodu:

// 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];

Obsługuje również selekcję, wykonując polecenie po wybraniu wiersza. Cały kod jest na moim blogu . Mam nadzieję, że to pomoże!

 4
Author: ColinE,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 10:33:50