2013-07-09 8 views
28

Sto lavorando all'integrazione di RAC nel mio progetto con l'obiettivo di creare un livello ViewModel che consentirà un facile caching/precaricamento dalla rete (oltre a tutti gli altri vantaggi di MVVM). Non ho ancora familiarità con MVVM o FRP e sto cercando di sviluppare un modello piacevole e riutilizzabile per lo sviluppo iOS. Ho un paio di domande su questo.Un modello ViewModel per app iOS con ReactiveCocoa

In primo luogo, questo è un po 'come ho aggiunto un ViewModel a una delle mie visualizzazioni, solo per provarlo. (Voglio questo qui per fare riferimento più tardi).

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

Ho ragione nel modo in cui sto usando i segnali? In particolare, ha senso avere bioSignal per aggiornare i dati e un hiddenBioSignal per associare direttamente alla proprietà nascosta di un textView?

Il mio primario domanda viene con preoccupazioni commoventi che sarebbero stati gestiti dai delegati nel ViewModel (si spera). I delegati sono così comuni nel mondo di iOS che mi piacerebbe trovare la soluzione migliore, o anche solo moderatamente praticabile.

Per un UITableView, ad esempio, è necessario fornire sia un delegato sia un dataSource. Devo avere una proprietà sul mio controller NSUInteger numberOfRowsInTable e collegarlo a un segnale sul ViewModel? E non sono molto chiaro su come utilizzare RAC per fornire il mio TableView con celle in tableView: cellForRowAtIndexPath:. Ho solo bisogno di fare questi il ​​modo "tradizionale" o è possibile avere una sorta di fornitore di segnale per le cellule? O forse è meglio lasciarlo com'è, perché un ViewModel non dovrebbe davvero preoccuparsi di costruire le viste, semplicemente modificando la fonte delle viste?

Inoltre, esiste un approccio migliore rispetto al mio utilizzo di un oggetto (fetchDoctorSubject)?

Qualsiasi altro commento sarebbe apprezzato anche. L'obiettivo di questo lavoro è quello di creare un livello ViewModel di prefetching/caching che possa essere segnalato ogni volta che è necessario per caricare i dati in background e quindi ridurre i tempi di attesa sul dispositivo. Se qualcosa di riutilizzabile viene fuori da questo (diverso da uno schema), sarà ovviamente open source.

Edit: E un'altra domanda: Sembra che in base alla documentazione, che dovrebbe usare le proprietà per tutti i segnali del mio ViewModel, invece di metodi? Penso che dovrei configurarli in init? O dovrei lasciarlo così com'è, in modo che i getter restituiscano nuovi segnali?

Devo avere una proprietà active come nell'esempio ViewModel nell'account github di ReactiveCocoa?

risposta

36

Il modello di vista dovrebbe modellare la vista. Il che vuol dire che non dovrebbe dettare alcun aspetto visivo, ma la logica dietro qualunque sia l'aspetto della vista. Non dovrebbe sapere nulla della vista direttamente. Questo è il principio guida generale.

Su alcuni dettagli.

Sembra che in base alla documentazione, dovrei essere utilizzando le proprietà per tutti i segnali del mio ViewModel, invece di metodi? Penso che dovrei configurarli in init? O dovrei lasciarlo così com'è, in modo che i getter restituiscano nuovi segnali?

Sì, in genere si utilizzano solo proprietà che rispecchiano le proprietà del modello. Noi li avevamo configuriamo in -init piace un pò:

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

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

    return self;  
} 

ricordare che modelli vista sono solo modelli per un uso specifico. Semplici oggetti antichi con semplici vecchie proprietà.

Ho ragione nel modo in cui sto usando i segnali? In particolare, ha senso avere bioSignal per aggiornare i dati e un hiddenBioSignal per associare direttamente alla proprietà nascosta di un textView?

Se nascondimento del segnale di bio è guidato da una logica modello specifico, che sarebbe senso per esporla come una proprietà sul modello di vista. Ma cerca di non pensarlo in termini di vista come l'occultamento. Forse si tratta più di validità, caricamento, ecc. Qualcosa non legato specificamente a come viene presentato.

Per un UITableView, ad esempio, è necessario fornire sia un delegato che un datasource. Dovrei avere una proprietà sul mio controller NSUInteger numberOfRowsInTable e collegarla a un segnale sul ViewModel? E non sono molto chiaro su come utilizzare RAC per fornire il mio TableView con le celle in tableView: cellForRowAtIndexPath :. Ho solo bisogno di fare questi il ​​modo "tradizionale" o è possibile avere una sorta di fornitore di segnale per le cellule? O forse è meglio lasciarlo com'è, perché un ViewModel non dovrebbe davvero preoccuparsi di costruire le viste, semplicemente modificando la fonte delle viste?

L'ultima riga è esatta. Il modello di visualizzazione dovrebbe fornire al controller di visualizzazione i dati da visualizzare (una matrice, un set, qualunque), ma il controller di visualizzazione è ancora il delegato e l'origine dati della vista tabella. Il controller della vista crea celle, ma le celle vengono popolate dai dati del modello di vista. Potresti anche avere un modello di visualizzazione delle celle se le tue celle sono relativamente complesse.

Inoltre, esiste un approccio migliore rispetto al mio utilizzo di un oggetto (fetchDoctorSubject)?

Considerare invece l'utilizzo di RACCommand qui. Ti darà un modo più piacevole di gestire richieste, errori e sicurezza thread simultanei. I comandi sono un modo abbastanza tipico di comunicare dalla vista al modello di vista.

Devo avere una proprietà attiva come nell'esempio ViewModel nell'account github di ReactiveCocoa?

Dipende solo se ne avete bisogno. Su iOS è probabilmente meno necessario di OS X, in cui è possibile avere più viste e visualizzare i modelli allocati ma non "attivi" contemporaneamente.

Speriamo che questo sia stato utile. Sembra che tu stia andando nella direzione giusta in generale!

+1

Questo è molto utile, grazie! Penso che RACCommand sia quello che volevo, e gli altri tuoi commenti hanno chiarito molti altri problemi. –

+1

Josh, grazie per averlo scritto, è super utile come sempre vedere questi esempi del mondo reale decostruiti. – cbowns

4

Per un UITableView, ad esempio, è necessario fornire sia un delegato che a dataSource. Dovrei avere una proprietà sul mio controller NSUInteger numberOfRowsInTable e collegarlo a un segnale sul ViewModel?

L'approccio standard, come descritto da joshaber above è quello di attuare manualmente l'origine dati e delegato all'interno del controller della vista, con il modello vista semplicemente esponendo una matrice di elementi ciascuno dei quali rappresenta un modello di vista che sostiene una cella di vista tabella .

Tuttavia, questo risulta in un lotto della piastra della caldaia nel vostro controller altrimenti elegante vista.

ho creato un simple binding helper che permette di associare un NSArray di modelli di visualizzazione per una vista tabella con poche righe di codice:

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

Gestisce anche la selezione, l'esecuzione di un comando quando una riga è selezionato. Il codice completo è over on my blog. Spero che questo ti aiuti!