I just started updating my ReactiveCocoa application to use the MVVM template and ask a few questions regarding the border between the ViewController and the ViewModel and how dumb the ViewController should be.
The first part of the updated application is the input stream, which behaves as follows.
- The user enters an email address, password and touches the login button.
- Successful response contains one or more models.
User - These models are
Userdisplayed along with the exit button. - Model A
Usermust be selected for the session before closing the login and presenting the main view.
Before MVVM
LoginViewControllerdirectly processes the command LoginButton
commandLoginButton negotiates directly with SessionManagerLoginViewControllerdisplays a UIActionSheetfor model selection Useror logout- The user selection and exit functions
LoginViewControllerspeak directly toSessionManager
After MVVM
LoginViewModel provides user login and selection commands and exit methodsLoginViewModel user selection and logout methods are directly related to SessionManagerLoginViewController responds to a login command LoginViewModelLoginViewControllerdisplays a UIActionSheetfor model selection Useror logout- User select and exit functions
LoginViewControllercommunicate withLoginViewModel
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);
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];
}];
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);
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;
[[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];
}
}];
[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"];
[users eachWithIndex:^(User *user, NSUInteger index) {
[sheet addButtonWithTitle:user.organisationName block:^{
[self.viewModel switchToUserAtIndex:index];
}];
}];
[sheet addCancelButtonWithTitle:@"Logout" block:^{
[self.viewModel logout];
}];
[sheet showInView:self.view];
}
@end
Questions
- Creating an extra layer of ViewModel means I need to proxy calls
SessionManager. I believe that the advantages of separation LoginViewControllerfrom SessionManageroutweighs the additional code challenges and ViewModel level functions? LoginViewController User, , . MVVM , , . LoginViewModel User, LoginViewController, , LoginViewController? LoginViewModel, , , LoginViewController ? , ViewModel , . , , , .LoginViewModel , SessionManager, LoginViewModel SessionManager?