How to properly separate ViewModel and ViewController in RAC MVVM

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 command
  • LoginButton negotiates directly with SessionManager
  • LoginViewControllerdisplays 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 methods
  • LoginViewModel user selection and logout methods are directly related to SessionManager
  • LoginViewController responds to a login command LoginViewModel
  • LoginViewControllerdisplays 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);

        // 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

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?
+4
1

, , .

1) / , . MVVM , View/Controller Model ViewModel.

View/Controller :

, View/Controller - -, .

, - -.

2) MVVM : View/controller, CollectionView/TableView, , - . , ViewModel, ViewModels.

Collection/Table, . View Model ViewModels, . №1, , - .

3) / View Model . , , - , / Session Manager unit test.

+1

Source: https://habr.com/ru/post/1537133/


All Articles