Как правильно разделить ViewModel и ViewController в RAC MVVM
Я только начал обновлять свое приложение ReactiveCocoa, чтобы использовать шаблон MVVM, и у меня есть несколько вопросов, касающихся границы между ViewController и ViewModel и того, насколько глупым должен быть ViewController.
Первая часть приложения, которое я обновляю, - это поток входа в систему, который ведет себя следующим образом.
- Пользователь вводит адрес электронной почты, пароль и нажимает кнопку входа
- Успешный ответ содержит один или несколько
User
модели - Эти
User
модели отображаются вместе с кнопкой выхода User
модель должна быть выбрана для сеанса до того, как представление входа в систему будет закрыто, и будет представлен основной вид.
До МВВМ
LoginViewController
непосредственно обрабатываетLoginButton
командаLoginButton
Командование говорит непосредственно сSessionManager
LoginViewController
отображаетUIActionSheet
для выбораUser
модель или выход- Функции выбора и выхода пользователя из
LoginViewController
поговорить напрямую сSessionManager
После МВВМ
LoginViewModel
выставляет команду входа в систему и выбор пользователя и методы выходаLoginViewModel
методы выбора пользователя и выхода из системы напрямую связаны сSessionManager
LoginViewController
реагирует на команду входа в системуLoginViewModel
LoginViewController
отображаетUIActionSheet
для выбораUser
модель или выход- Функции выбора и выхода пользователя из
LoginViewController
поговорить сLoginViewModel
LoginViewModel.h
@interface LoginViewModel : RVMViewModel
@property (strong, nonatomic, readonly) RACCommand *loginCommand;
@property (strong, nonatomic, readonly) RACSignal *checkingSessionSignal;
@property (strong, nonatomic, readonly) NSArray *users;
@property (strong, nonatomic) NSString *email;
@property (strong, nonatomic) NSString *password;
- (void)logout;
- (void)switchToUserAtIndex:(NSUInteger)index;
@end
LoginViewModel.m
@implementation LoginViewModel
- (instancetype)init {
self = [super init];
if (self) {
@weakify(self);
// Set up the login command
self.loginCommand = [[RACCommand alloc] initWithEnabled:[self loginEnabled]
signalBlock:^RACSignal *(id input) {
@strongify(self);
[[[SessionManager sharedInstance] loginWithEmail:self.email
password:self.password]
subscribeNext:^(NSArray *users) {
self.users = users;
}];
return [RACSignal empty];
}];
// Observe the execution state of the login command
self.loggingIn = [[self.loginCommand.executing first] boolValue];
}
return self;
}
- (void)logout {
[[SessionManager sharedInstance] logout];
}
- (void)switchToUserAtIndex:(NSUInteger)index {
if (index < [self.users count]) {
[[SessionManager sharedInstance] switchToUser:self.users[index]];
}
}
- (RACSignal *)loginEnabled {
return [RACSignal
combineLatest:@[
RACObserve(self, email),
RACObserve(self, password),
RACObserve(self, loggingIn)
]
reduce:^(NSString *email, NSString *password, NSNumber *loggingIn) {
return @([email length] > 0 &&
[password length] > 0 &&
![loggingIn boolValue]);
}];
}
@end
LoginViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
@weakify(self);
// Bind to the view model
RAC(self.controlsContainerView, hidden) = self.viewModel.checkingSessionSignal;
RAC(self.viewModel, email) = self.emailField.rac_textSignal;
RAC(self.viewModel, password) = self.passwordField.rac_textSignal;
self.loginButton.rac_command = self.viewModel.loginCommand;
self.forgotPasswordButton.rac_command = self.viewModel.forgotPasswordCommand;
// Respond to the login command execution
[[RACObserve(self.viewModel, users)
skip:1]
subscribeNext:^(NSArray *users) {
@strongify(self);
if ([users count] == 0) {
[Utils presentMessage:@"Sorry, there appears to be a problem with your account."
withTitle:@"Login Error"
level:MessageLevelError];
} else if ([users count] == 1) {
[self.viewModel switchToUserAtIndex:0];
} else {
[self showUsersList:users];
}
}];
// Respond to errors from the login command
[self.viewModel.loginCommand.errors
subscribeNext:^(id x) {
[Utils presentMessage:@"Sorry, your login credentials are incorrect."
withTitle:@"Login Error"
level:MessageLevelError];
}];
}
- (void)showUsersList:(NSArray *)users {
CCActionSheet *sheet = [[CCActionSheet alloc] initWithTitle:@"Select Organization"];
// Add buttons for each of the users
[users eachWithIndex:^(User *user, NSUInteger index) {
[sheet addButtonWithTitle:user.organisationName block:^{
[self.viewModel switchToUserAtIndex:index];
}];
}];
// Add a button for cancelling/logging out
[sheet addCancelButtonWithTitle:@"Logout" block:^{
[self.viewModel logout];
}];
// Display the action sheet
[sheet showInView:self.view];
}
@end
Вопросы
- Создание дополнительного слоя ViewModel означает, что мне нужно прокси
SessionManager
звонки. Я думаю, выгода от разделенияLoginViewController
отSessionManager
перевешивает дополнительный код и вызовы функций слоя ViewModel? LoginViewController
имеет знания оUser
модель для отображения списка пользователей, которые могут быть выбраны. Это нарушает схему MVVM и, конечно, не кажется правильным. ЕслиLoginViewModel
извлечь только необходимые свойстваUser
модель требуетсяLoginViewController
и добавить их в словарь, массив которого возвращается вLoginViewController
? Или было бы лучше иметь метод наLoginViewModel
который возвращает имя пользователя с указанным индексом, позволяяLoginViewController
отобразить это имя? Я понимаю, что ViewModel отвечает за преодоление разрыва между моделью и представлением, однако это похоже на двойную обработку. Что касается моей догадки в первом вопросе, я предполагаю, что преимущества разделения этих проблем намного перевешивают то, что кажется немного трудоемким процессом картирования.- Если
LoginViewModel
вызывает все функциональные возможности, содержащиеся вSessionManager
достаточно ли написать тесты противLoginViewModel
только или должны тесты также быть написаны специально противSessionManager
?
1 ответ
Уже довольно поздно, и я уверен, что вы пошли дальше.
1) перемещение логики программы из вида / элемента управления всегда стоит дополнительных нескольких конусов, которые вам нужно записать в прокси. Смысл MVVM состоит в том, чтобы поощрять разделение интересов и обеспечивать четкий канал данных между View/Controller и Model через ViewModel.
С точки зрения View/Controller, ваши View Models должны выполнять следующую функцию:
Выступать в качестве черного ящика данных, который ваш View/Controller может использовать без выполнения каких-либо бизнес-правил, и всегда предполагать, что данные верны.
Выступать в качестве канала для обработки пользовательского ввода, который принимает этот пользовательский ввод без выполнения каких-либо бизнес-правил.
2) В моих реализациях MVVM я пытаюсь следовать этой парадигме: представление / контроллер, содержащий CollectionView/TableView, является родительским представлением, а ячейки - дочерними представлениями. Таким образом, у вас должен быть родительский ViewModel, который должен инициализировать дочерние ViewModels и управлять ими.
В вашем случае вы не используете представление Коллекция / Таблица, но концепция та же самая. Вам следует попросить у вашего родительского View Model список дочерних ViewModels, которые вы можете передать в другое представление для использования. Следуя пункту ответа № 1, родительская модель представления должна обеспечить правильную инициализацию дочерних моделей представления, чтобы дочернему представлению не приходилось беспокоиться о проверке данных.
3) При тестировании проверки / правил данных вашей модели представления вы можете полностью отключить Session Manager и протестировать только модель представления. Что я делаю, так это создаю утверждения, что заштрихованные / смоделированные функции Session Manager вызываются соответствующим образом в моем модульном тесте.