Cómo separar correctamente ViewModel y ViewController en RAC MVVM

Acabo de comenzar a actualizar mi aplicación ReactiveCocoa para usar el patrón MVVM y tengo algunas preguntas sobre el límite entre ViewController y ViewModel y cuán tonto debería ser el ViewController.

La primera parte de la aplicación que estoy actualizando es el flujo de inicio de session, que se comporta de la siguiente manera.

  • El usuario ingresa una dirección de correo electrónico, contraseña y toca el button de inicio de session
  • Una respuesta exitosa contiene uno o más models de User
  • Estos models de User se muestran junto con un button de cerrar session.
  • Se debe seleccionar un model de User para la session antes de que se cierre la vista de inicio de session y se presente la vista principal.

Antes de MVVM

  • LoginViewController maneja directamente el command LoginButton
  • LoginButton command LoginButton habla directamente al SessionManager
  • LoginViewController muestra una UIActionSheet de UIActionSheet para seleccionar un model de User o cerrar session.
  • Las funciones de selección y cierre de session del usuario de LoginViewController hablan directamente con SessionManager

Después de MVVM

  • LoginViewModel expone un command de inicio de session y methods de selección y cierre de session de usuario.
  • LoginViewModel methods de selección de usuario y cierre de session de LoginViewModel hablan directamente con SessionManager
  • LoginViewController reactjs al command de inicio de session de LoginViewModel
  • LoginViewController muestra una UIActionSheet de UIActionSheet para seleccionar un model de User o cerrar session.
  • Las funciones de selección y cierre de session del usuario de LoginViewController hablan con 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 shanetworkingInstance] 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 shanetworkingInstance] logout]; } - (void)switchToUserAtIndex:(NSUInteger)index { if (index < [self.users count]) { [[SessionManager shanetworkingInstance] switchToUser:self.users[index]]; } } - (RACSignal *)loginEnabled { return [RACSignal combineLatest:@[ RACObserve(self, email), RACObserve(self, password), RACObserve(self, loggingIn) ] networkinguce:^(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 cnetworkingentials 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 

Preguntas

  1. La creación de la capa ViewModel adicional significa que tengo que proxy las llamadas de SessionManager . Supongo que la ventaja de desacoplar LoginViewController del SessionManager supera el código adicional y las llamadas a funciones de la capa ViewModel?
  2. El LoginViewController tiene conocimiento del model de User para mostrar una list de usuarios que pueden seleccionarse. Esto rompe el patrón de MVVM y ciertamente no se siente bien. ¿Debería el LoginViewModel extraer solo las properties necesarias de un model de User requerido por el LoginViewController y agregarlas a un dictionary, una matriz de las cuales se devuelve al LoginViewController ? ¿O sería mejor tener un método en LoginViewModel que devuelva el nombre de un usuario dado un índice, lo que permite que LoginViewController muestre este nombre? Entiendo que ViewModel es responsable de cerrar la brecha entre el model y la vista; sin embargo, esto se siente como un doble manejo. Según mi presentimiento en la primera pregunta, creo que los beneficios de separar estas preocupaciones superan con creces lo que parece un process de mapeo un poco laborioso.
  3. Si LoginViewModel llama a todas las funciones incluidas en el SessionManager es suficiente para escribir testings solo en LoginViewModel o las testings también deben escribirse específicamente contra SessionManager ?

Esto es bastante tarde, y estoy seguro de que has seguido adelante.

1) mover la lógica del progtwig fuera de una vista / control siempre vale la pena las pocas líneas adicionales de cono que necesita escribir en el proxy. El objective de MVVM es fomentar la separación de preocupaciones y proporcionar un canal claro de datos entre la Vista / Controlador y el Modelo a través del ViewModel.

Desde la perspectiva del View / Controller, su View Models debería realizar la siguiente function:

Actúe como un recuadro negro de datos que su View / Controller puede aprovechar sin realizar ninguna regla comercial y siempre supondrá que los datos son correctos.

Actúa como un conducto para el procesamiento de input de usuario que toma esa input de usuario sin tener que realizar ninguna regla comercial.

2) En mis implementaciones de MVVM, bash y sigo este paradigma: Una vista / controller que contiene una CollectionView / TableView es una vista principal y las celdas son vistas secundarias. Por lo tanto, debe tener un ViewModel padre cuyo trabajo es inicializar y administrar ViewModels hijo.

En su caso, no está utilizando una vista Colección / Tabla, pero el concepto es el mismo. Debería pedirle a su padre que vea Model una list de Child ViewModels que pueda pasar a otra vista para aprovechar. Siguiendo el punto en la respuesta # 1, el model de vista padre debería asegurarse de que Child ViewModels se inicializa correctamente para que la vista secundaria no tenga que preocuparse por ninguna validation de datos.

3) Al probar las reglas / validation de datos de su Modelo de Vista, puede ocultar totalmente el Administrador de Sesión y solo probar el Modelo de Vista. Lo que hago es crear aseveraciones de que las funciones de Session Manager simuladas / burladas se llaman apropiadamente en mi testing de unidad.