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?
Questo è molto utile, grazie! Penso che RACCommand sia quello che volevo, e gli altri tuoi commenti hanno chiarito molti altri problemi. –
Josh, grazie per averlo scritto, è super utile come sempre vedere questi esempi del mondo reale decostruiti. – cbowns