Я работаю над интеграцией RAC в свой проект с целью создания слоя ViewModel, который позволит легко кэшировать/предварительно загружать из сети (плюс все другие преимущества MVVM). Я еще не знаком с MVVM или FRP, и я пытаюсь разработать хороший, многоразовый шаблон для разработки iOS. У меня есть пара вопросов об этом.
Во-первых, это то, как я добавил ViewModel в один из моих представлений, просто чтобы попробовать. (Я хочу, чтобы это здесь упоминалось позже).
В представлении ViewControllerDidLoad:
@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, например, нам необходимо предоставить как делегат, так и источник данных. Должен ли я иметь свойство на контроллере NSUInteger numberOfRowsInTable
и привязывать его к сигналу на ViewModel? И я действительно не понимаю, как использовать RAC для обеспечения моего TableView ячейками в tableView: cellForRowAtIndexPath:
. Нужно ли мне просто делать это "традиционным" способом или возможно иметь какой-то провайдер сигналов для ячеек? Или, может быть, лучше оставить его как есть, потому что ViewModel не должен действительно заботиться о создании представлений, просто изменяя источник представлений?
Кроме того, есть ли лучший подход, чем мое использование предмета (fetchDoctorSubject)?
Любые другие комментарии также будут оценены. Целью этой работы является создание уровня preMetching/кэширования ViewModel, который может сигнализироваться при необходимости для загрузки данных в фоновом режиме и, таким образом, сокращения времени ожидания на устройстве. Если из этого выйдет что-нибудь многоразовое (кроме шаблона), оно, конечно, будет открытым исходным кодом.
Изменить: И еще один вопрос: похоже, согласно документации, я должен использовать свойства для всех сигналов в моей модели ViewModel вместо методов? Думаю, я должен настроить их в init? Или я должен оставить его как есть, чтобы геттеры возвращали новые сигналы?
Должен ли я иметь свойство active
, как в примере ViewModel в учетной записи gigub ReactiveCocoa?