【问题标题】:How to correctly separate the ViewModel and ViewController in RAC MVVM如何正确分离 RAC MVVM 中的 ViewModel 和 ViewController
【发布时间】:2014-04-18 19:19:51
【问题描述】:

我刚刚开始更新我的 ReactiveCocoa 应用程序以使用 MVVM 模式,并且对 ViewController 和 ViewModel 之间的边界以及 ViewController 应该有多笨有一些疑问。

我要更新的应用程序的第一部分是登录流程,其行为如下。

  • 用户输入电子邮件地址、密码并点击登录按钮
  • 成功的响应包含一个或多个 User 模型
  • 这些User 模型与注销按钮一起显示
  • 在关闭登录视图并显示主视图之前,必须为会话选择User 模型。

MVVM 之前

  • LoginViewController 直接处理LoginButton 命令
  • LoginButton 命令直接与SessionManager 对话
  • LoginViewController 显示UIActionSheet 用于选择User 模型或注销
  • LoginViewController的用户选择和注销功能直接与SessionManager对话

MVVM 之后

  • LoginViewModel 公开了登录命令以及用户选择和注销方法
  • LoginViewModel 用户选择和注销方法直接与SessionManager 对话
  • LoginViewControllerLoginViewModel 的登录命令作出反应
  • 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

问题

  1. 创建额外的 ViewModel 层意味着我需要代理 SessionManager 调用。我想将LoginViewControllerSessionManager 解耦的好处超过了 ViewModel 层的额外代码和函数调用?
  2. LoginViewController 了解User 模型,以便显示可以选择的用户列表。这打破了 MVVM 模式,当然感觉不对。 LoginViewModel 是否应该只提取LoginViewController 所需的User 模型的必要属性并将它们添加到字典中,其中的数组返回到LoginViewController?或者在LoginViewModel 上有一个方法会更好,它返回给定索引的用户名,允许LoginViewController 显示这个名称?我知道 ViewModel 负责弥合模型和视图之间的差距,但这确实感觉像是双重处理。根据我对第一个问题的预感,我猜想分离这些关注点的好处远远超过了感觉有点费力的映射过程。
  3. 如果LoginViewModel 调用包含在SessionManager 中的所有功能,是否只针对LoginViewModel 编写测试就足够了,还是应该专门针对SessionManager 编写测试?

【问题讨论】:

    标签: ios mvvm reactive-cocoa


    【解决方案1】:

    现在已经很晚了,我相信你已经继续前进了。

    1) 将程序逻辑移出视图/控件总是值得您需要写入代理的额外几行锥体。 MVVM 的目的是鼓励关注点分离,并通过 ViewModel 在 View/Controller 和 Model 之间提供清晰的数据通道。

    从视图/控制器的角度来看,您的视图模型应该执行以下功能:

    充当您的视图/控制器可以在不执行任何业务规则的情况下利用的数据黑盒,并始终假定数据是正确的。

    充当用户输入处理的管道,无需执行任何业务规则即可接受用户输入。

    2) 在我的 MVVM 实现中,我尝试遵循以下范例:包含 CollectionView/TableView 的视图/控制器是父视图,而单元格是子视图。因此,您应该有一个父 ViewModel,其工作是初始化和管理子 ViewModel。

    在您的情况下,您没有使用 Collection/Table 视图,但概念是相同的。您应该向您的父视图模型询问子视图模型的列表,您可以将其传递到另一个视图以加以利用。按照答案 #1 中的要点,父 View Model 应确保正确初始化子 ViewModel,以便子 View 无需担心任何数据验证。

    3) 在测试您的视图模型的数据验证/规则时,您可以完全取消会话管理器并仅测试视图模型。我所做的是创建断言,在我的单元测试中适当地调用存根/模拟会话管理器函数。

    【讨论】:

    • 我有一个问题。我希望你能对此有所了解。假设我在登录后进行了 API 调用以检索一些数据。我将调用 API 和返回结果的相关代码放在 LoginViewModel 中的视图中。在另一个视图中的应用程序中,我需要调用相同的 API 并获取数据。我是否在该视图中引用 LoginViewModel 并立即调用该方法?我觉得这很奇怪,因为我指的是一个不属于该特定视图的视图模型类,只是为了调用一个方法。
    • 我是否将这些重复方法提取到某种通用视图模型或其他东西中?
    • 视图模型应该只被一个视图使用。在您的示例中,您将有两个类,每个类都调用您的身份验证 API。如果每个实现都有大量代码,这可能是一个信号,表明您应该将 API 调用包装到帮助程序/集成类中。您绝对不希望视图模型中的业务规则与视图无关。
    • 感谢您的回复。我确实有在单独的类中调用 API 的方法。可以从两个不同的视图模型调用此方法吗?代码会重复,这是我的担心。
    猜你喜欢
    • 2016-07-17
    • 2015-08-24
    • 1970-01-01
    • 1970-01-01
    • 2012-05-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-03-13
    相关资源
    最近更新 更多