在饿了么蜂鸟团队版项目组工作了有半年时间了,从我刚进来的时候,团队版就已经开始转型为MVVM+RAC的方式来作为底层核心框架,但对于一个iOS开发新人的我来说,以前只是熟悉MVC的开发模式,并且对于RAC从未接触,突然要开始接触全新的理念无疑是有点困难的,刚开始的两个月时间就是熟悉项目组的各种基础框架以及业务需求逻辑,说实话对于底层框架,对我非科班出身的应届生来说,基本算是云里雾里,但是从多位前辈大佬的口中得知刚开始不要想着一口吃个胖子,可以先从抄代码开始,也就是动手实践,后面的时间慢慢的跟着做业务迭代,开始熟悉这个框架的基本用法,只是模仿着怎么去用,但是从前人的口中知道刚开始就是先学会用,再慢慢去理解,我后来也觉得是这样。这个框架是之前iOS组的leader从零开始写起来的,来来回回我也看了好几遍,现在终于有些理解其中的真谛,心里还是挺开心的。
首先我们来看一下这个库的基本结构
一、MVVM基本结构:
层 | 定义 | |
---|---|---|
1 | Model | 数据结构 |
2 | View | View 或者 ViewController,一般情况下在 ViewController 中进行 View 与 ViewModel 之间的数据绑定,如果 View 是 UITableViewCell 和 UICollectionViewCell 等,也会在 View 中进行数据绑定 |
3 | ViewModel | 管理ViewController的生命周期,网络请求获取数据模型,响应用户操作(导航,点击,滑动,手势等) |
4 | Service | 这一层提供系统依赖的外部接口,如网络调用层、系统定位等 |
5 | Controls | 这一层提供一些工具类 |
要让 ViewController 廋下来的,就需要将对应的业务逻辑移到 ViewModel 层,主要把导航从VC映射到VM,所有需要push,pop,present,dismiss操作的接口都封装到Navigation相关的两个protocol:LPDNavigationControllerProtocol ,LPDNavigationViewModelProtocol 中,当需要 Present 或者 Push 一个 ViewController,必须要嵌套在 NavigationViewController 中,同样的 Present 或者 Push 一个 ViewModel 时,必须要嵌套在 NavigationViewModel中,这样并不会带来更多的复杂性,但是在需要用 Navigation 时,不需要做任何改动。实际上导航操作还是原生的方法处理的,顺带映射到VM中的导航栈数组变化
ViewModel 与 ViewController 解藕存在的问题 | 解决方案对应的 Protocol | |
---|---|---|
1 | 导航同步问题 | LPDNavigationControllerProtocolLPDNavigationViewModelProtocol |
2 | 子ViewController问题 | LPDViewControllerProtocol, LPDViewModelProtocol |
3 | 生命周期同步问题 | LPDViewModelBecomeActiveProtocol, LPDViewModelDidLoadViewProtocol,LPDViewModelDidLayoutSubviewsProtocol |
4 | 表单提交进度条 | LPDViewModelSubmittingProtocol |
5 | 加载进度条,网络较差重试页面 | LPDViewModelLoadingProtocol |
6 | toast | LPDViewModelToastProtocol |
7 | empty | LPDViewModelEmptyProtocol |
8 | networkstatus | LPDViewModelNetworkStatusProtocol |
9 | 下拉刷新、上拉加载更多 | LPDScrollViewModelProtocol, LPDScrollViewControllerProtocol |
该框架中用了大量的protocol,主要是将一些同类的方法集中声明起来,在该用的地方服从协议并实现
二、具体分析
LPDNavigationController分析
通过RAC的方式去封锁可能出现的导航操作的所有方式,包括两种可能:
1.在LPDNavigationController及其子类中调用push,pop,present,dismiss
2.在LPDNavigationViewModel及其子类中调用push,pop,present,dismiss
- 一对协议(LPDNavigationControllerProtocol, LPDNavigationViewModelProtocol)
@protocol LPDNavigationControllerProtocol
@required
- (instancetype)initWithViewModel:(__kindof id)viewModel;
@property (nullable, nonatomic, strong, readonly) __kindof id viewModel;
- (void)presentNavigationController:(UINavigationController *)viewControllerToPresent animated: (BOOL)flag completion:(void (^ __nullable)(void))completion;
- (void)dismissNavigationControllerAnimated: (BOOL)flag completion: (void (^ __nullable)(void))completion;
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated;
- (nullable UIViewController *)popViewControllerAnimated:(BOOL)animated;
- (nullable NSArray<__kindof UIViewController *> *)popToViewController:(UIViewController *)viewController animated:(BOOL)animated;
- (nullable NSArray<__kindof UIViewController *> *)popToRootViewControllerAnimated:(BOOL)animated;
- (void)setViewControllers:(NSMutableArray *)viewControllers animated:(BOOL)animated;
@property(nullable, nonatomic,readonly,strong) UIViewController *topViewController;
@property(nullable, nonatomic,readonly,strong) UIViewController *visibleViewController;
@property(nonatomic,copy) NSArray<__kindof UIViewController *> *viewControllers;
@end
@protocol LPDNavigationViewModelProtocol
@required
- (instancetype)initWithRootViewModel:(__kindof id)viewModel;
@property (nullable, nonatomic, strong, readonly) __kindof id topViewModel;
@property (nullable, nonatomic, strong, readonly) __kindof id visibleViewModel;
@property (nullable, nonatomic, strong) __kindof id presentedViewModel;
@property (nullable, nonatomic, strong) __kindof id presentingViewModel;
@property (nonatomic, strong, readonly) NSArray<__kindof id> *viewModels;
@property (nullable, nonatomic, strong) __kindof id tabBar;
@optional
- (void)pushViewModel:(__kindof id)viewModel animated:(BOOL)animated;
- (void)popViewModelAnimated:(BOOL)animated;
- (void)popToViewModel:(__kindof id)viewModel animated:(BOOL)animated;
- (void)popToRootViewModelAnimated:(BOOL)animated;
- (void)presentNavigationViewModel:(__kindof id)viewModel
animated:(BOOL)animated
completion:(nullable void (^)())completion;
- (void)dismissNavigationViewModelAnimated:(BOOL)animated completion:(nullable void (^)())completion;
- (void)setViewModels:(NSMutableArray > *)viewModels animated:(BOOL)animated;
@end
- @brief 设置同步ViewModel的导航到ViewController的导航的信号
- (void)subscribePushSignals {
@weakify(self);
[[self rac_signalForSelector:@selector(pushViewController:animated:)]
subscribeNext:^(RACTuple *tuple) {
@strongify(self);
__kindof id viewControllerToPush = tuple.first;
if ([viewControllerToPush isKindOfClass:LPDViewController.class] &&
[viewControllerToPush respondsToSelector:@selector(viewModel)] &&
[self.viewModel respondsToSelector:@selector(_pushViewModel:)]) {
[self.viewModel performSelector:@selector(_pushViewModel:) withObject:viewControllerToPush.viewModel];
}
}];
[[[self.viewModel rac_signalForSelector:@selector(pushViewModel:animated:)] deliverOnMainThread]
subscribeNext:^(RACTuple *tuple) {
@strongify(self);
id viewController =
(id)[LPDViewControllerFactory viewControllerForViewModel:tuple.first];
[self pushViewController:viewController animated:[tuple.second boolValue]];
}];
}
导航操作对应的逻辑示意图:
LPDTabBarController分析
- 一对协议(LPDTabBarControllerProtocol,LPDTabBarViewModelProtocol)
@protocol LPDTabBarControllerProtocol
@required
- (instancetype)initWithViewModel:(__kindof id)viewModel;
@property (nullable, nonatomic, strong, readonly) __kindof id viewModel;
@end
@protocol LPDTabBarViewModelProtocol
@required
- (instancetype)initWithViewModels:(NSArray<__kindof id> *)viewModels;
@property (nonatomic, readonly, strong) __kindof id selectedViewModel;
@property (nonatomic) NSUInteger selectedIndex;
@property (nonatomic, readonly, strong) NSMutableArray<__kindof id> *viewModels;
@end
- LPDTabBarController和LPDTabBarViewModel的绑定
- (instancetype)initWithViewModel:(__kindof id)viewModel {
self = [super init];
if (self) {
self.viewModel = viewModel;
NSMutableArray *viewControllers = [NSMutableArray array];
for (id childViewModel in self.viewModel.viewModels) {
UIViewController *viewController = [LPDViewControllerFactory viewControllerForViewModel:childViewModel];
[viewControllers addObject:viewController];
}
self.viewControllers = viewControllers;
RAC(self, selectedIndex) = RACObserve(self.viewModel, selectedIndex);
}
return self;
}
LPDTabBarController一般和LPDNavigationController配对使用
LPDViewController分析
- 一对协议(LPDViewControllerProtocol,LPDViewModelProtocol)
@protocol LPDViewControllerProtocol
@required
- (instancetype)initWithViewModel:(__kindof id)viewModel;
@property (nullable, nonatomic, strong, readonly) __kindof id viewModel;
@property(nullable, nonatomic,readonly,strong) UINavigationController *navigationController;
@property(nonatomic,readonly) NSArray<__kindof UIViewController *> *childViewControllers NS_AVAILABLE_IOS(5_0);
- (void)addChildViewController:(UIViewController *)childController NS_AVAILABLE_IOS(5_0);
- (void)removeFromParentViewController NS_AVAILABLE_IOS(5_0);
@protocol LPDViewModelProtocol
@required
/**
* @brief navigation bar title
*/
@property (nullable, nonatomic, copy) NSString *title;
/**
* @brief 添加childViewModel,如果此viewModel对应的viewController还没有加载,
* 则会先加载到childViewModels中,等对应的viewController加载后会将所有childViewModels
* 中的viewModel对应的的viewController加载
*/
- (void)addChildViewModel:(id)childViewModel;
/**
* @brief 从父viewModel中移除
*/
- (void)removeFromParentViewModel;
/**
* @brief childViewModels,从对应viewController中的addChildViewController方法可以添加
* 或者通过viewModel的addChildViewModel方法添加
*/
@property (nonatomic, copy, readonly) NSArray> *childViewModels;
@property (nonatomic, weak, readonly) id parentViewModel;
/**
* @brief navigation view model
*/
@property (nullable, nonatomic, weak) __kindof id navigation;
@end
- 实现从LPDViewController到LPDViewModel的绑定
例如(均类似这种):
1.生命周期函数的绑定(
LPDViewModelBecomeActiveProtocol,
LPDViewModelDidLoadViewProtocol,
LPDViewModelDidLayoutSubviewsProtocol,
)
- (void)subscribeActiveSignal {
@weakify(self);
[[self rac_signalForSelector:@selector(viewWillAppear:)] subscribeNext:^(id x) {
@strongify(self);
self.viewModel.active = YES;
}];
[[self rac_signalForSelector:@selector(viewWillDisappear:)] subscribeNext:^(id x) {
@strongify(self);
self.viewModel.active = NO;
}];
}
2.业务逻辑的绑定(
LPDViewModelLoadingProtocol,
LPDViewModelSubmittingProtocol,
LPDViewModelToastProtocol,
LPDViewModelEmptyProtocol,
LPDViewModelNetworkStatusProtocol
)
- (void)subscribeLoadingSignal {
@weakify(self);
[[RACObserve(self.viewModel, loading) skip:1] subscribeNext:^(id x) {
@strongify(self);
if ([x boolValue]) {
[self showLoading];
} else {
[self hideLoading];
}
}];
}
LPDScrollViewController分析
主要负责下拉刷新和上拉加载
LPDScrollViewController : LPDViewController
LPDScrollViewModel : LPDViewModel
- 一对协议(LPDScrollViewControllerProtocol,LPDScrollViewModelProtocol)
@protocol LPDScrollViewControllerProtocol
/**
* @brief 设置下拉刷新当前页面上的数据,生效当且仅当scrollView被有效赋值
* 当赋值为YES,可以出现下拉刷新控件,为NO时不出现,默认为NO
*/
@property (nonatomic, assign) BOOL needLoadingHeader;
/**
* @brief 设置列表中上滑加载更多数据,生效当且仅当loadingView被有效赋值
* 当赋值为YES,可以出现上拉加载更多控件,为NO时不出现,默认为NO
*/
@property (nonatomic, assign) BOOL needLoadingFooter;
/**
* @brief scrollView,可以是UIScrollView,
* UITableView,UICollectionView,UIWebView等
* loadingView赋值后,可以设置needLoading和needLoadingMore
*/
@property (nullable, nonatomic, weak) __kindof UIScrollView *scrollView;
@optional
/**
* @brief 初始化下拉刷新Header
*/
- (MJRefreshHeader *)customLoadingHeader:(MJRefreshComponentRefreshingBlock)refreshingBlock;
/**
* @brief 初始化上拉加载Footer
*/
- (MJRefreshFooter *)customLoadingFooter:(MJRefreshComponentRefreshingBlock)refreshingBlock;
@end
@protocol LPDScrollViewModelProtocol
@end
- 下拉刷新和上拉加载的绑定
LPDScrollViewModel下拉刷新中的setLoading和setLoadingSignal方法重用了LPDViewModel中的,上拉加载要重新写两个set方法
- (void)subscribeNeedLoadingHeaderSignal {
@weakify(self);
[[RACObserve(self, needLoadingHeader) filter:^BOOL(id value) {
@strongify(self);
return self.scrollView != nil;
}] subscribeNext:^(id x) {
@strongify(self);
if ([x boolValue]) {
if (nil == self.loadingHeader) {
MJRefreshComponentRefreshingBlock refreshingBlock = ^{
@strongify(self);
self.viewModel.loading = YES;
};
if ([self respondsToSelector:@selector(customLoadingHeader:)]) {
self.loadingHeader = [self customLoadingHeader:refreshingBlock];
} else {
self.loadingHeader = [MJRefreshNormalHeader headerWithRefreshingBlock:refreshingBlock];
}
self.scrollView.mj_header = self.loadingHeader;
}
} else {
self.scrollView.mj_header = nil;
self.loadingHeader = nil;
}
}];
}
- (void)subscribeLoadingSignal {
@weakify(self);
[[[RACObserve(self.viewModel, loading) skip:1] filter:^BOOL(id value) {
@strongify(self);
return nil != self.scrollView;
}] subscribeNext:^(id x) {
@strongify(self);
if (self.needLoadingHeader) {
if ([x boolValue]) {
[self.viewModel setEmpty:NO];
if (!self.loadingHeader.isRefreshing) { // 非下拉刷新触发
[self performSelector:@selector(showLoading)];
}
} else {
if (self.loadingHeader.isRefreshing) {
[self.loadingHeader endRefreshing];
} else {
[self performSelector:@selector(hideLoading)];
}
}
} else {
if ([x boolValue]) {
[self.viewModel setEmpty:NO];
[self performSelector:@selector(showLoading)];
} else {
[self performSelector:@selector(hideLoading)];
}
}
}];
}
- (void)subscribeNeedLoadingFooterSignal {
@weakify(self);
[[RACObserve(self, needLoadingFooter) filter:^BOOL(id value) {
@strongify(self);
return self.scrollView != nil;
}] subscribeNext:^(id x) {
@strongify(self);
if ([x boolValue]) {
if (nil == self.loadingFooter) {
MJRefreshComponentRefreshingBlock refreshingBlock = ^{
@strongify(self);
[self.viewModel setLoadingMoreState:LPDLoadingMoreStateBegin];
};
if ([self respondsToSelector:@selector(customLoadingFooter:)]) {
self.loadingFooter = [self customLoadingFooter:refreshingBlock];
} else {
self.loadingFooter = [MJRefreshAutoNormalFooter footerWithRefreshingBlock:refreshingBlock];
}
self.scrollView.mj_footer = self.loadingFooter;
}
} else {
self.scrollView.mj_footer = nil;
self.loadingFooter = nil;
}
}];
}
- (void)subscribeLoadingMoreSignal {
@weakify(self);
[[RACObserve(((id)self.viewModel), loadingMoreState) filter:^BOOL(id value) {
@strongify(self);
return nil != self.scrollView;
}] subscribeNext:^(NSNumber *value) {
@strongify(self);
LPDLoadingMoreState loadingMoreState = [value integerValue];
if (loadingMoreState == LPDLoadingMoreStateBegin) {
[self.loadingFooter beginRefreshing];
} else if (loadingMoreState == LPDLoadingMoreStateEnd){
[self.loadingFooter endRefreshing];
} else {
[self.loadingFooter noticeNoMoreData];
}
}];
}
三、用法举例
(1) 导航一一对应的例子
LPDHomeViewModel *vm = [[LPDHomeViewModel alloc] init];
LPDHomeViewController *vc = [[LPDHomeViewController alloc] initWithViewModel:vm];
[self.navigation pushViewController:vc animated:YES];
LPDHomeViewModel *vm = [[LPDHomeViewModel alloc] init];
[self.navigation pushViewModel:vm animated:YES];
[self.navigation popViewControllerAnimated:YES];
[self.navigation popViewModelAnimated:YES];
[self.navigation popToRootViewControllerAnimated];
[self.navigation popToRootViewModelAnimated:YES];
LPDHomeViewModel *vm = [[LPDHomeViewModel alloc] init];
LPDNavigationViewModel *nvm = [[LPDNavigationViewModel alloc] initWithRootViewModel:vm];
[self.navigation presentViewController:[[LPDNavigationController alloc] initWithViewModel:nvm] animated:YES completion:nil];
LPDHomeViewModel *vm = [[LPDHomeViewModel alloc] init];
[self.navigation presentViewModel:[[LPDNavigationViewModel alloc] initWithRootViewModel:vm] animated:YES completion:nil];
[self.navigation dismissViewControllerAnimated:YES completion:nil];
[self.navigation dismissViewModelAnimated:YES completion:nil];
(2) 子 ViewController 的例子
要实现子 ViewController,现在可以这么做了:
LPDWaybillsViewModel *waybillsViewModel = [[LPDWaybillsViewModel alloc] init];
waybillsViewModel.title = @"待取餐";
waybillsViewModel.waybillStatus = LPDWaybillStatusFetching;
[self addChildViewModel:waybillsViewModel];
waybillsViewModel = [[LPDWaybillsViewModel alloc] init];
waybillsViewModel.title = @"待送达";
waybillsViewModel.waybillStatus = LPDWaybillStatusDelivering;
[self addChildViewModel:waybillsViewModel];
(3) 表单提交的进度条的例子
更简单了,一行代码:
self.submitting = YES; // Show
self.submitting = NO; // hide
(4) 加载的进度条的例子
需要设置 BeginLoadingBlock 和 EndLoadingBlock 来实现显示和取消加载进度条,然后不需要做其它事情了,剩下的交给框架来实现就好了,后面会说到 Tableview 和 Collectionview 加载的实现:
[self beginLoadingBlock:^(UIView *_Nonnull view) {
UIView *contentView = [view viewWithTag:777777];
if (contentView) {
return;
}
contentView = [[UIView alloc] initWithFrame:view.bounds];
contentView.tag = 777777;
contentView.backgroundColor = [UIColor clearColor];
[view addSubview:contentView];
UIView *loadingView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
loadingView.layer.cornerRadius = 10;
loadingView.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.8];
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 57, 42)];
imageView.animationImages = @[
[UIImage imageNamed:@"01"],
[UIImage imageNamed:@"02"],
[UIImage imageNamed:@"03"],
[UIImage imageNamed:@"04"],
[UIImage imageNamed:@"05"],
[UIImage imageNamed:@"06"]
];
[loadingView addSubview:imageView];
imageView.center = CGPointMake(loadingView.width / 2, loadingView.height / 2);
[contentView addSubview:loadingView];
// loadingView.center = CGPointMake(contentView.width / 2, contentView.height / 2);
if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 8) {
loadingView.center = [[UIApplication sharedApplication]
.keyWindow convertPoint:CGPointMake(UIScreen.width / 2, UIScreen.height / 2)
toView:view];
} else {
loadingView.center = CGPointMake([UIApplication sharedApplication].keyWindow.center.x,
[UIApplication sharedApplication].keyWindow.center.y - 64);
}
[imageView startAnimating];
}];
[self endLoadingBlock:^(UIView *_Nonnull view) {
UIView *contentView = [view viewWithTag:777777];
if (contentView) {
[contentView removeFromSuperview];
}
}];
(5) 下拉刷新的例子
下拉刷新默认使用 MJRefresh,可以扩展然后通过 InitHeaderBlock 来定制自己的下拉刷新效果,要实现下拉刷新不要太简单了,在LPDScrollViewController 的子类中添加两行代码,在对应的 ViewModel 中实现 LoadingSignal:
self.scrollView = self.tableView;
self.needLoading = YES;
(6) 上拉加载更多的例子
上拉加载更多默认使用 MJRefresh,目前暂时不支持定制,上拉加载更多的实现也很简单,在 LPDScrollViewController 的子类中添加一行代码,剩下的就是在对应的 ViewModel 中实现 LoadingMoreSignal:
self.needLoadingMore = YES;
该库确实做到了降低vc的重量,将逻辑放到vm中来,但是该库依然有一些还需改进的地方,我们会一直维护下去。
以下贴出源码地址:
LPDMvvmKit