我刚刚开始更新我的ReactiveCocoa应用程序以使用MVVM模式,并且有一些关于ViewController和ViewModel之间边界的问题以及ViewController应该是多么愚蠢。
我正在更新的应用程序的第一部分是登录流程,其行为如下。
User
模型User
模型以及退出按钮User
模型。在MVVM之前
LoginViewController
直接处理LoginButton
命令LoginButton
命令直接与SessionManager
LoginViewController
显示UIActionSheet
,用于选择User
模型或退出LoginViewController
的用户选择和退出功能直接与SessionManager
MVVM之后
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
问题
SessionManager
次调用。我想将LoginViewController
与SessionManager
分离的好处超过了ViewModel层的额外代码和函数调用?LoginViewController
了解User
模型,以便显示可以选择的用户列表。这打破了MVVM模式,当然感觉不对。 LoginViewModel
是否应仅提取User
所需的LoginViewController
模型的必要属性并将其添加到字典中,其数组将返回到LoginViewController
?或者在LoginViewModel
上有一个方法会更好,它返回给定索引的用户名,允许LoginViewController
显示此名称?我知道ViewModel负责弥合模型和视图之间的差距,但这确实感觉像是双重处理。根据我在第一个问题中的预感,我认为分离这些问题的好处远远超过了稍微费力的映射过程。LoginViewModel
调用SessionManager
中包含的所有功能,仅仅针对LoginViewModel
编写测试就足够了,还是应该针对SessionManager
专门编写测试? 答案 0 :(得分:1)
现在已经很晚了,我相信你已经离开了。
1)从视图/控件中移出程序逻辑总是值得你需要写入代理的额外几行锥形。 MVVM的目的是鼓励关注点分离,并通过ViewModel在View / Controller和Model之间提供清晰的数据通道。
从View / Controller的角度来看,View Models应该执行以下功能:
充当您的视图/控制器可以在不执行任何业务规则的情况下利用的黑匣子数据,并始终假设数据是正确的。
充当用户输入处理的渠道,无需执行任何业务规则即可获取用户输入。
2)在我的MVVM实现中,我尝试遵循这个范例:包含CollectionView / TableView的视图/控制器是父视图,单元格是子视图。因此,您应该拥有一个父ViewModel,其职责是初始化和管理子ViewModel。
在您的情况下,您没有使用Collection / Table视图,但概念是相同的。您应该向父视图模型询问子ViewModel的列表,您可以将其传递到另一个视图以进行利用。根据答案#1中的要点,父视图模型应确保正确初始化子视图模型,以便子视图不需要担心任何数据验证。
3)在测试View Model的数据验证/规则时,您可以完全存根会话管理器并仅测试视图模型。我所做的是创建断言,在我的单元测试中适当地调用存根/模拟的会话管理器函数。