Подтвердить что ты не робот

Шаблон ViewModel для приложений iOS с ReactiveCocoa

Я работаю над интеграцией 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?

4b9b3361

Ответ 1

Модель представления должна моделировать представление. Иными словами, он не должен диктовать внешний вид самого вида, но логика любого вида внешнего вида. Он не должен знать ничего о представлении напрямую. Это общий руководящий принцип.

Включение в некоторые особенности.

Похоже, согласно документации, я должен использовать свойства для всех сигналов в моей модели 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, например, нам необходимо предоставить как делегат, так и источник данных. Должен ли я иметь свойство на контроллере NSUInteger numberOfRowsInTable и привязывать его к сигналу на ViewModel? И я действительно не понимаю, как использовать RAC для обеспечения моего TableView ячейками в tableView: cellForRowAtIndexPath:. Нужно ли мне просто делать это "традиционным" способом или возможно иметь какой-то провайдер сигналов для ячеек? Или, может быть, лучше оставить его как есть, потому что ViewModel не должен действительно заботиться о создании представлений, просто изменяя источник представлений?

Эта последняя строка в точности верна. Ваша модель представления должна предоставить контроллеру представления данные для отображения (массив, набор, независимо), но ваш контроллер представления по-прежнему является делегатом представления таблиц и источником данных. Контроллер представления создает ячейки, но ячейки заполняются данными из модели представления. У вас может быть даже модель ячейки, если ваши клетки относительно сложны.

Кроме того, есть ли лучший подход, чем мое использование предмета (fetchDoctorSubject)?

Рассмотрим вместо этого RACCommand. Это даст вам более удобный способ обработки одновременных запросов, ошибок и безопасности потоков. Команды являются довольно типичным способом передачи информации из представления в модель представления.

Должен ли я иметь активное свойство, как в примере ViewModel в учетной записи gigum ReactiveCocoa?

Это зависит от того, нужно ли вам это. На iOS это, вероятно, менее необходимо, чем OS X, где вы можете иметь несколько видов и просматривать модели, но не "активно" сразу.

Надеюсь, это было полезно. Похоже, вы вообще направляетесь в правильном направлении!

Ответ 2

Для UITableView, например, нам необходимо предоставить как делегат, так и источник данных. Должен ли я иметь свойство на моем контроллере 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];

Он также обрабатывает выбор, выполняя команду при выборе строки. Полный код в моем блоге. Надеюсь, это поможет!