GitBucket源码阅读笔记

学习函数式编程已经有一个半月了,学习了RxSwift和RAC2.X,尝试过写一些小的demo,但是一直没有了解过MVVM中是如何使用RAC进行通信的,由于公司项目准备使用MVVM+RAC模式重构,在github找到了使用MVVM+RAC构建的Github开源app:GitBucket,本文主要记录该项目中RAC在MVVM模式中的应用。

项目类图


项目中主要有两大继承体系:MRCViewModel和MRCViewController,分别对应ViewModel层和Controller层,实现代码复用

在MRCViewModel父类中存放了一些需发送给Controller层的信号,如标题变化,错误信号,控制器隐藏信号等;
MRCViewController中实现了监听ViewModel信号的方法,如监听ViewModel的标题变化

1
2
3
4
5
6
7
8
9
10
11
RAC(self.navigationItem, titleView) = [RACObserve(self.viewModel, titleViewType).distinctUntilChanged map:^(NSNumber *value) {
MRCTitleViewType titleViewType = value.unsignedIntegerValue;
switch (titleViewType) {
case MRCTitleViewTypeDefault:
return titleView;
case MRCTitleViewTypeDoubleTitle:
return (UIView *)doubleTitleView;
case MRCTitleViewTypeLoadingTitle:
return (UIView *)loadingTitleView;
}
}];

另外项目中使用了Service层提供ViewModel所需的各种服务
OCTClient:Github的三方库api,提供了RAC的支持
MRCRepositoryService/MRCRepositoryServiceImpl:应用自有的服务类
MRCViewModelServices:提供界面跳转的实现

界面跳转

作者没有使用系统提供的push/present操作实现界面跳转,而是在MRCViewModelServices中实现了一系列空操作

1
2
3
4
5
6
7
8
9
10
11
- (void)pushViewModel:(MRCViewModel *)viewModel animated:(BOOL)animated {}

- (void)popViewModelAnimated:(BOOL)animated {}

- (void)popToRootViewModelAnimated:(BOOL)animated {}

- (void)presentViewModel:(MRCViewModel *)viewModel animated:(BOOL)animated completion:(VoidBlock)completion {}

- (void)dismissViewModelAnimated:(BOOL)animated completion:(VoidBlock)completion {}

- (void)resetRootViewModel:(MRCViewModel *)viewModel {}

之后在视图层维护了一个MRCNavigationControllerStack,通过RAC订阅Service的跳转方法的调用,提供真正的界面跳转的代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@weakify(self)
[[(NSObject *)self.services
rac_signalForSelector:@selector(pushViewModel:animated:)]
subscribeNext:^(RACTuple *tuple) {
@strongify(self)

MRCViewController *topViewController = (MRCViewController *)[self.navigationControllers.lastObject topViewController];
if (topViewController.tabBarController) {
topViewController.snapshot = [topViewController.tabBarController.view snapshotViewAfterScreenUpdates:NO];
} else {
topViewController.snapshot = [[self.navigationControllers.lastObject view] snapshotViewAfterScreenUpdates:NO];
}

UIViewController *viewController = (UIViewController *)[MRCRouter.sharedInstance viewControllerForViewModel:tuple.first];
viewController.hidesBottomBarWhenPushed = YES;
[self.navigationControllers.lastObject pushViewController:viewController animated:[tuple.second boolValue]];
}];

这样Controller只需要做数据绑定即可,需要跳转界面时,ViewModel层调用service的方法,MRCNavigationControllerStack监听到方法调用后做界面跳转
[self.services pushViewModel:viewModel animated:YES];

另外由于VM层不可引入View层的类,所以跳转的一系列方法的传参都为ViewModel,通过维护一个ViewModel与Controller层的映射字典,实现了视图与界面的分离
@property (nonatomic, copy) NSDictionary *viewModelViewMappings; // viewModel到view的映射

#RAC用法

###通过RAC提供代理的函数实现

使用rac_signalForSelector:fromProtocol方法订阅键盘的回车键

1
2
3
4
5
6
7
8
@weakify(self)
[[self
rac_signalForSelector:@selector(textFieldShouldReturn:)
fromProtocol:@protocol(UITextFieldDelegate)]
subscribeNext:^(RACTuple *tuple) {
@strongify(self)
if (tuple.first == self.passwordTextField) [self.viewModel.loginCommand execute:nil];
}];

###RAC监听UI事件
使用rac_signalForControlEvents方法订阅按钮的点击信号,当按钮被点击时,执行登录的命令

1
2
3
4
5
6
[[self.loginButton
rac_signalForControlEvents:UIControlEventTouchUpInside]
subscribeNext:^(id x) {
@strongify(self)
[self.viewModel.loginCommand execute:nil];
}];

###RAC监听通知
使用rac_addObserverForName方法订阅app从后台进入前台的通知,takeUntil保证在对象销毁后取消订阅

1
2
3
4
5
6
7
[[[[NSNotificationCenter defaultCenter]
rac_addObserverForName:UIApplicationWillEnterForegroundNotification object:nil]
takeUntil:self.rac_willDeallocSignal]
subscribeNext:^(id x) {
@strongify(self)
[self.viewModel.requestRemoteDataCommand execute:nil];
}];

###RAC在TableView中的应用
监听ViewModel中dataSource数组的变化,dataSource发生改变时重新加载tableView

1
2
3
4
5
6
7
8
@weakify(self)
[[[RACObserve(self.viewModel, dataSource)
distinctUntilChanged]
deliverOnMainThread]
subscribeNext:^(id x) {
@strongify(self)
[self reloadData];
}];

刷新列表时显示loading菊花,刷新完成时隐藏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[[[RACSignal
combineLatest:@[ self.viewModel.requestRemoteDataCommand.executing, RACObserve(self.viewModel, dataSource) ]
reduce:^(NSNumber *executing, NSArray *dataSource) {
return @(executing.boolValue && dataSource.count == 0);
}]
deliverOnMainThread]
subscribeNext:^(NSNumber *showHUD) {
@strongify(self)
if (showHUD.boolValue) {
[MBProgressHUD showHUDAddedTo:self.view animated:YES].labelText = MBPROGRESSHUD_LABEL_TEXT;
} else {
[MBProgressHUD hideHUDForView:self.view animated:YES];
}
}];

使用rac_sequence和map将model转化为viewmodel数据源

1
2
3
4
5
6
7
8
9
10
11
12
13
- (NSArray *)dataSourceWithEvents:(NSArray *)events {
if (events.count == 0) return nil;

@weakify(self)
NSArray *viewModels = [events.rac_sequence map:^(OCTEvent *event) {
@strongify(self)
MRCNewsItemViewModel *viewModel = [[MRCNewsItemViewModel alloc] initWithEvent:event];
viewModel.didClickLinkCommand = self.didClickLinkCommand;
return viewModel;
}].array;

return @[ viewModels ];
}