iOS MVVM+RAC
〇、更新
針對最後一小節中幹掉基類的做法(Runtime + category),已經有所實現,並按照自己的思路新寫了個列表實現,具體可查看對於iOS架構模式之爭的一些思考。
一、前言
很早之前就想寫寫自己在架構模式方面的心得,但是一直感覺自己是井底之蛙,畢竟在iOS領域越深入越感到自己的無知,心中有著敬畏之心,就更沒有自信去寫這個東西(你也可以理解是沒時間(>﹏<),請原諒我的裝逼,嘿嘿).
對於架構模式這個讓人又愛又恨的玩意,說來其實簡單,但一千個人眼中就有一千種哈姆雷特,說他千變萬化確實是事實,而且當你深入其中的時候你真的會上癮,並樂此不疲!
前幾天自己寫的一篇《iOS Xcode全面剖析》閱讀量在短短一天內破千,還上了簡書首頁(你看這句話字體就知道不是廣告了( ⊙o⊙ ))
,確實很開心,昨天又跟我一朋友用代碼講解了我對MVVM的理解及運用,此情此景下,腦袋一熱搞出一篇來分享給大家也情有可原,當然更希望有更多的大神來指點一下,讓我自己也讓大家有提升就夠了,萬分感謝!
二、談談MVVM和RAC
1、MVVM淺析
到這裏我就默認你看過MVVM相關文章(畢竟相關文章已經可以用滿天飛來形容了~(≧▽≦)/~啦啦啦!),僅僅簡要談談我對其的理解。
MVC是構建iOS App的標準模式,是蘋果推薦的一個用來組織代碼的權威範式,市面上大部分App都是這樣構建的,具體組建模式不細說,iOS入門者都比較了解(雖然不一定能完全去遵守),但其幾個不能避免的問題卻是很嚴重困擾開發者比如厚重的ViewController、遺失的網絡邏輯(沒有屬於它的位置)、較差的可測試性
MVVM雖然來自微軟,但是不應該反對它,它正式規範了正式規範了視圖和控制器緊耦合的性質,如下圖:
MVVM圖示
ViewModel: 相比較於MVC新引入的視圖模型。是視圖顯示邏輯、驗證邏輯、網絡請求等代碼存放的地方,唯一要註意的是,任何視圖本身的引用都不應該放在VM中,換句話說就是VM中不要引入UIKit.h (對於image這個,也有人將其看做數據來處理,這就看個人想法了,並不影響整體的架構)。
這樣,首先解決了VC臃腫的問題,將邏輯代碼、網絡請求等都寫入了VM中,然後又由於VM中包含了所有的展示邏輯而且不會引用V,所以它是可以通過編程充分測試的。
so,就是這個樣子的,6666!
2、RAC淺淺析
特別淺。。。本文重點是框架及實戰及MVVM思想,RAC這玩意話說學習曲線較長,難以理解,不好上手,是因為之前學習的時候使用者、中文教程還比較少,所以學習運用起來比較費勁,(當時確實廢了好大得勁,實力裝逼一把 @%&$%& )但現在已經成熟的爛大街了,只要有心,好的教程一大把,能潛下心來看我寫的水文的人,拿下RAC不在話下!
ReactiveCocoa 可以說是結合了函數式編程和響應式編程的框架,也可稱其為函數響應式編程(FRP)框架,強調一點,RAC雖然最大的優點是提供了一個單一的、統一的方法去處理異步的行為,包括delegate方法,blocks回調,target-action機制,notifications和KVO.但是不要簡單的只是單純的認為他僅僅就是減少代碼復雜度,更好的配合MVVM而已,小夥子,這樣你就小看它了。
它最大的與眾不同是提供了一種新的寫代碼的思維,由於RAC將Cocoa中KVO、UIKit event、delegate、selector等都增加了RAC支持,所以都不用去做很多跨函數的事。
如果全工程都使用RAC來實現,對於同一個業務邏輯終於可以在同一塊代碼裏完成了,將UI事件,邏輯處理,文件或數據庫操作,異步網絡請求,UI結果顯示,這一大套統統用函數式編程的思路嵌套起來,進入頁面時搭建好這所有的關系,用戶點擊後妥妥的等著這一套聯系一個個的按期望的邏輯和次序觸發,最後顯示給用戶。
額,就說這麽多,再說就沒頭了~(≧▽≦)/~啦啦啦!
3、本篇對兩者的理解運用
在此次介紹中,會使用MVVM+RAC結合的方式,搞定一個添加上拉加載及下拉刷新的列表,所以更多的詮釋MVVM思想,而不是RAC的邏輯鏈式操作(這一點用登錄界面來寫更能體現Y^o^Y ),RAC在此扮演的更大一部分的角色是更好的解耦,減少代碼復雜度,使代碼層次分明、邏輯清晰更便於維護升級。
二、框架部分
1、框架目錄詳解
首先介紹一下本框架的目錄結構,如下圖
1、Frameworks
存放系統庫的虛擬文件夾, 目前搭建框架的時候需要手動添加一個名稱為Frameworks的虛擬文件夾,這樣你在Build Phases 中添加的系統庫會自動歸入此文件夾,不會直接在外部顯示以至於打亂目錄結構。系統庫添加流程如下:
另外,細心地家夥會發現此目錄中有兩個相同的Frameworks, 那這到底是什麽鬼?最上面的那個Frameworks是在自己搭框架自己添加的,當時的項目還很單純, 沒有這麽淘氣,問題出在下面那個Pods Target上,添加它之後就會自動給你生成一個虛擬的Frameworks的文件夾,那又該問了為啥不直接用下面那個呢???(廢話真多!反正也沒沖突,就留著吧╮(╯﹏╰)╭)
既然提到了Pods,那接下來講講CocoaPods(第三方類庫管理工具)。
2、CocoaPods
當你開發iOS應用時,會經常使用到很多第三方開源類庫,比如JSONKit,AFNetWorking等等。可能某個類庫又用到其他類庫,所以要使用它,必須得另外下載其他類庫,而其他類庫又用到其他類庫,“子子孫孫無窮盡也”,反正在早期我是體會過這種痛苦,好心酸,手動一個個去下載所需類庫是十分麻煩的。
還有另外一種常見情況是,你項目中用到的類庫有更新,你必須得重新下載新版本,重新加入到項目中,十分麻煩。
CocoaPods就是幫你解決上面的問題的,話說這玩意應該是iOS最常用最有名的類庫管理工具了,作為iOS程序員的我們,掌握CocoaPods的使用是必不可少的基本技能了,至於這玩意該咋用?
O(∩_∩)O哈哈~你覺得我會告訴你麽?好吧,我這人還是很心軟的,下面一張圖告訴你該咋用...(?乛?乛? 磨人的小妖精)
?(??????)哎呦,不錯哦~是不是get了一個新技能 ?6666!
3、AppDelegate
這個目錄下放的是AppDelegate.h(.m)文件,是整個應用的入口文件,所以單獨拿出來。一會兒告訴你如何寫一個簡潔的AppDelegate,會在這個文件夾裏添加一些類,所以將其放入一個文件夾內還是很有必要的。
4、Class
工程主體類, 日常大部分開發代碼均在這裏,又細分了好多次級目錄。
通用類
-
General : 通用類(文件夾項目移植過程中都不需要更改的就能直接使用的)
- Base : 基類 (整個框架的基類)
- Categories : 公共擴展類 (就是一些常用的類別,比如分享啊什麽的)
- Core : 公共核心類(一般存放個人信息、接口API等)
- Models : 公共Model (公用的一些數據模型)
- Views : 公共View (封裝的一些常用的View)
工具類
- Helpers : 工程的相關輔助類(比如類似數據請求、表單上傳、網絡監測等工具類)
宏定義類
-
Macro : 宏定義類 (就是整個應用會用到的宏定義)
- AppMacro.h app項目的相關宏定義
- NotificationMacro.h 通知相關的宏定義
- VendorMacro.h 第三方相關宏定義
- UtilsMacro.h 為簡化代碼的宏定義
- ...等等等等(其他隨你定啦!Y^o^Y )
APP具體模塊代碼類
-
Sections : 各模塊的文件夾(一般而言,我們以人為單位)
- LSSections 王隆帥的文件夾
- CLSections 馬成麟的文件夾
- ...等等等等(也可以寫你最喜歡的蒼老師的,叼叼的!)
每個成員的文件夾下是其所負責模塊的文件夾,比如蒼老師負責PHP界面模塊(我也認為PHP是最好的語言!大家可以在評論區談論一下!?乛?乛? 磨人的小妖精),如下(接著上面的個人文件夾):
-
PHP : 模塊名,也可以是首頁(HomePage)...等等
- ViewControllers 界面控制器存放處(這是文件夾名)
- ViewModels 打雜的(MVVM的核心、解耦合、處理邏輯等)
- Views 界面相關View存放處(界面相關子View)
- Models 數據模型存放處(各種單純的數據模型,一點都不胖,是標準的瘦Model)
這就是標準的MVVM了。。。為啥不和上面目錄連起來呢?為啥呢?為啥呢?因為臣妾做不到啊!!!(不會三級、四級列表的MarkDown寫法,求大神支招!良辰必有重謝!)
第三方類庫
- Vendors : 第三方的類庫/SDK,如UMeng、WeiboSDK、WeixinSDK等等。
到這哥們又該疑惑了,心裏該碎碎念了:(????д????)????? What are you 弄啥嘞!剛才剛講了個第三方庫管理CocoaPods,你丫這裏自己又搞了一個,?( ˉ?? ˉ?) 信不信我突突了你!
哈哈哈,剛才的CocoaPods確實管理著大部分的第三方庫,這裏建立第三方庫目錄的原因有兩個:其一,並不是所有的你需要的第三方都支持pods的,所以還是需要手動添加一些類庫。其二,一些第三方庫雖然支持pods,但是需要我們去更改甚至自定義這個第三方,此時也需要放入這裏,也防止使用pods一不小心更新掉你的自定義!?(?)? 你來打我啊!
5、Resource
這裏放置的是工程所需的一些資源,如下
- Fonts 字體
- Images 圖片(當然你可以添加至Assets.xcassets, 沒人攔著你)
- Sounds 聲音
- Videos 視頻
ok,目錄就講到這裏!想知道更詳細的可以私信我!
2、基類詳解
這裏著重講解一下VC、V、VM的基類,其他的模式與View類似所以略過,其中TableViewCell的基類稍微特殊所以也提一下。
我目前的基類如下圖:
是不是眼花繚亂了..., 我曾經也看它不順眼, 曾經嘗試過把基類都幹掉,然後遇到了一些麻煩...就妥協了,在文章的最後可以跟大家聊聊我是怎麽去幹掉基類,然後又失敗的,這裏先詳細講一下基類。
1、YDViewController
函數的具體用意圖已經標的很清楚了,這裏簡單講一下四個函數的作用
-
yd_addSubviews : 添加View到ViewController
-
yd_bindViewModel : 用來綁定V(VC)與VM
-
yd_layoutNavigation : 設置導航欄、分欄
-
yd_getNewData : 初次獲取數據的時候調用(不是特別必要)
2、YDView
- yd_setupViews : 添加子View到主View
- yd_bindViewModel : 綁定V與VM
- yd_addReturnKeyBoard : 設置點擊空白鍵盤回收
3、YDViewModel
- yc_initialize : 進行一些邏輯綁定,網絡數據請求處理。
-
LSRefreshDataStatus 數據處理後需要進行的操作標識
- LSHeaderRefresh_HasMoreData 下拉還有更多數據
- LSHeaderRefresh_HasNoMoreData 下拉沒有更多數據
- LSFooterRefresh_HasMoreData 上拉還有更多數據
- LSFooterRefresh_HasNoMoreData 上拉沒有更多數據
- LSRefreshError 刷新出錯
- LSRefreshUI 僅僅刷新UI布局
4、YDTableViewCell
由於Cell比較特殊,所以單拎出來說一下。觀察上面的ViewMdoel、View等的基類會發現每個基類都會有數據綁定的地方,但是cell得數據綁定需要放在數據初始化的時候,因為所有的基類的數據邏輯綁定都是在沒有返回初始化對象的時候調用的,但是cell中假如在那裏面進行數據綁定會出現問題比如下圖:
cell復用失敗
上圖中的函數假如是在 bindViewModel
內,則會復用失敗,點擊按鈕是沒有反應的,但是假如是在數據初始化的時候調用:比如 setViewModel
的時候,就會OK了,因為裏面用到了cell的在RAC中復用機制 rac_prepareForReuseSignal
,在cell還沒有初始化返回的時候是失效的。
3、題外話
基類的作用是統一管理,統一風格,便於編碼,有更多的額外的附加功能的話,建議使用Protocol 或 Category,這樣移植性強,便於管理與擴展,不至於牽一發而動全身。
本篇基類核心是用VM來配置V(VC),並提供一些必須的Protocol方法來處理界面顯示、邏輯,將代碼風格規範化,各個部分的功能明朗化,這樣,當你需要寫什麽,需要找什麽,需要更改什麽的時候都會很明確這些代碼的位置,邏輯更清晰,而不會浪費更多的時間在思考應該寫在哪,該去哪找,要改的地方在哪這種不該費時間的問題上。
三、實戰部分(經典列表的實現)
這裏講一下如下界面的代碼構造方式,很普通的一個列表:(懶得再寫了,這是我之前做的一個項目的一個界面,之前基類講解中會看到都是YD開頭的,在這裏是YC開頭就這個區別而已)
首先觀察這個界面,需求是:頭部的內容數量多的話是可以左右滑動的,然後整體是可以上拉加載的。我是這樣處理的:首先界面整體是一個TableView,然後分為一個Header、一個Section和主體列表Row。在Header上嵌套一個CollectionView保證可復用。具體分層如下
然後處理完後的目錄如下:
簡單介紹一下:
-
ViewController
- LSCircleListViewController : 界面主控制器,負責跳轉、Navgation、TabBar等
-
View
- LSCircleListView : 界面主View,負責主要界面的顯示
- LSCircleListHeaderView : 頭部Header,封裝的內部含有一個CollectionView
- LSCircleListCollectionCell : 頭部Header中的CollectionView自定義的Cell
- LSCircleListSectionHeaderView : SectionView,此界面不需復用,所以單純一個View即可,若需要復用需要TableViewHeaderFooterView
- LSCircleListTableCell : 主TableView的Cell
-
ViewModel
- LSCircleListViewModel : 界面主ViewModel
- LSCircleListHeaderViewModel : 頭部Header對應的ViewModel
- LSCircleListCollectionCellViewModel : 頭部CollectionCell及TableViewCell的ViewModel(因為二者的數據結構是一致的)
- LSCircleListSectionHeaderViewModel : Section的ViewModel
-
Model
- LSCircleListModel : 圈子的數據模型(header和tableViewCell數據結構是一致的)
一個小小的界面這麽多類...是不是難以接受了,淡定些,騷年!你要想想把這些個東西都放在VC內是個什麽趕腳?也得好幾千行呢!(有點誇張!不過也夠頭疼的),這麽多類,這裏著重講一下主VC、主V、主VM、主M就ok,能詳細講明白MVVM之間是如何工作的就一通百通了。
1、LSCircleListViewController的處理
先上代碼:
//
// LSCircleListViewController.m
// ZhongShui
//
// Created by 王隆帥 on 16/3/10.
// Copyright ? 2016年 王隆帥. All rights reserved.
//
#import "LSCircleListViewController.h"
#import "LSCircleListView.h"
#import "LSCircleListViewModel.h"
#import "LSCircleMainPageViewController.h"
#import "LSCircleMainPageViewModel.h"
#import "LSCircleListCollectionCellViewModel.h"
#import "LSNewCircleListViewController.h"
@interface LSCircleListViewController ()
@property (nonatomic, strong) LSCircleListView *mainView;
@property (nonatomic, strong) LSCircleListViewModel *viewModel;
@end
@implementation LSCircleListViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
}
#pragma mark - system
- (void)updateViewConstraints {
WS(weakSelf)
[self.mainView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(weakSelf.view);
}];
[super updateViewConstraints];
}
#pragma mark - private
- (void)yc_addSubviews {
[self.view addSubview:self.mainView];
}
- (void)yc_bindViewModel {
@weakify(self);
[[self.viewModel.cellClickSubject takeUntil:self.rac_willDeallocSignal] subscribeNext:^(LSCircleListCollectionCellViewModel *viewModel) {
@strongify(self);
LSCircleMainPageViewModel *mainViewModel = [[LSCircleMainPageViewModel alloc] init];
mainViewModel.headerViewModel.circleId = viewModel.idStr;
mainViewModel.headerViewModel.headerImageStr = viewModel.headerImageStr;
mainViewModel.headerViewModel.title = viewModel.name;
mainViewModel.headerViewModel.numStr = viewModel.peopleNum;
LSCircleMainPageViewController *circleMainVC = [[LSCircleMainPageViewController alloc] initWithViewModel:mainViewModel];
[self.rdv_tabBarController setTabBarHidden:YES animated:YES];
[self.navigationController pushViewController:circleMainVC animated:YES];
}];
[self.viewModel.listHeaderViewModel.addNewSubject subscribeNext:^(id x) {
@strongify(self);
LSNewCircleListViewController *newCircleListVC = [[LSNewCircleListViewController alloc] init];
[self.rdv_tabBarController setTabBarHidden:YES animated:YES];
[self.navigationController pushViewController:newCircleListVC animated:YES];
}];
}
- (void)yc_layoutNavigation {
self.title = @"圈子列表";
[self.rdv_tabBarController setTabBarHidden:NO animated:YES];
}
#pragma mark - layzLoad
- (LSCircleListView *)mainView {
if (!_mainView) {
_mainView = [[LSCircleListView alloc] initWithViewModel:self.viewModel];
}
return _mainView;
}
- (LSCircleListViewModel *)viewModel {
if (!_viewModel) {
_viewModel = [[LSCircleListViewModel alloc] init];
}
return _viewModel;
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
/*
#pragma mark - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
// Get the new view controller using [segue destinationViewController].
// Pass the selected object to the new view controller.
}
*/
@end
對於VC,分為三個模塊,下面分別來說一下:
i 第一個模塊:系統函數
此函數是從iOS6.0開始在ViewController中新增一個更新約束布局的方法,這個方法默認的實現是調用對應View的 updateConstraints
。ViewController的View在更新視圖布局時,會先調用ViewController的updateViewConstraints 方法。我們可以通過重寫這個方法去更新當前View的內部布局,而不用再繼承這個View去重寫-updateConstraints方法。我們在重寫這個方法時,務必要調用 super 或者 調用當前View的 -updateConstraints 方法。
ⅱ 第二個模塊 : 私有函數
前面基類內也提到了這三個函數的具體作用,即
-
yd_addSubviews : 添加View到ViewController
-
yd_bindViewModel : 這裏綁定了兩個跳轉事件。
-
yd_layoutNavigation : 設置了標題為“圈子列表”、及TabBar不隱藏
ⅲ 第三個模塊 : 懶加載
這就不用解釋了,用到時再加載。
2、View的處理
先上代碼
//
// LSCircleListView.m
// ZhongShui
//
// Created by 王隆帥 on 16/3/10.
// Copyright ? 2016年 王隆帥. All rights reserved.
//
#import "LSCircleListView.h"
#import "LSCircleListViewModel.h"
#import "LSCircleListHeaderView.h"
#import "LSCircleListSectionHeaderView.h"
#import "LSCircleListTableCell.h"
@interface LSCircleListView () <UITableViewDataSource, UITableViewDelegate>
@property (strong, nonatomic) LSCircleListViewModel *viewModel;
@property (strong, nonatomic) UITableView *mainTableView;
@property (strong, nonatomic) LSCircleListHeaderView *listHeaderView;
@property (strong, nonatomic) LSCircleListSectionHeaderView *sectionHeaderView;
@end
@implementation LSCircleListView
/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect {
// Drawing code
}
*/
#pragma mark - system
- (instancetype)initWithViewModel:(id<YCViewModelProtocol>)viewModel {
self.viewModel = (LSCircleListViewModel *)viewModel;
return [super initWithViewModel:viewModel];
}
- (void)updateConstraints {
WS(weakSelf)
[self.mainTableView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(weakSelf);
}];
[super updateConstraints];
}
#pragma mark - private
- (void)yc_setupViews {
[self addSubview:self.mainTableView];
[self setNeedsUpdateConstraints];
[self updateConstraintsIfNeeded];
}
- (void)yc_bindViewModel {
[self.viewModel.refreshDataCommand execute:nil];
@weakify(self);
[self.viewModel.refreshUI subscribeNext:^(id x) {
@strongify(self);
[self.mainTableView reloadData];
}];
[self.viewModel.refreshEndSubject subscribeNext:^(id x) {
@strongify(self);
[self.mainTableView reloadData];
switch ([x integerValue]) {
case LSHeaderRefresh_HasMoreData: {
[self.mainTableView.mj_header endRefreshing];
if (self.mainTableView.mj_footer == nil) {
self.mainTableView.mj_footer = [MJRefreshBackNormalFooter footerWithRefreshingBlock:^{
@strongify(self);
[self.viewModel.nextPageCommand execute:nil];
}];
}
}
break;
case LSHeaderRefresh_HasNoMoreData: {
[self.mainTableView.mj_header endRefreshing];
self.mainTableView.mj_footer = nil;
}
break;
case LSFooterRefresh_HasMoreData: {
[self.mainTableView.mj_header endRefreshing];
[self.mainTableView.mj_footer resetNoMoreData];
[self.mainTableView.mj_footer endRefreshing];
}
break;
case LSFooterRefresh_HasNoMoreData: {
[self.mainTableView.mj_header endRefreshing];
[self.mainTableView.mj_footer endRefreshingWithNoMoreData];
}
break;
case LSRefreshError: {
[self.mainTableView.mj_footer endRefreshing];
[self.mainTableView.mj_header endRefreshing];
}
break;
default:
break;
}
}];
}
#pragma mark - lazyLoad
- (LSCircleListViewModel *)viewModel {
if (!_viewModel) {
_viewModel = [[LSCircleListViewModel alloc] init];
}
return _viewModel;
}
- (UITableView *)mainTableView {
if (!_mainTableView) {
_mainTableView = [[UITableView alloc] init];
_mainTableView.delegate = self;
_mainTableView.dataSource = self;
_mainTableView.backgroundColor = GX_BGCOLOR;
_mainTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
_mainTableView.tableHeaderView = self.listHeaderView;
[_mainTableView registerClass:[LSCircleListTableCell class] forCellReuseIdentifier:[NSString stringWithUTF8String:object_getClassName([LSCircleListTableCell class])]];
WS(weakSelf)
_mainTableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
[weakSelf.viewModel.refreshDataCommand execute:nil];
}];
_mainTableView.mj_footer = [MJRefreshAutoNormalFooter footerWithRefreshingBlock:^{
[weakSelf.viewModel.nextPageCommand execute:nil];
}];
}
return _mainTableView;
}
- (LSCircleListHeaderView *)listHeaderView {
if (!_listHeaderView) {
_listHeaderView = [[LSCircleListHeaderView alloc] initWithViewModel:self.viewModel.listHeaderViewModel];
_listHeaderView.frame = CGRectMake(0, 0, SCREEN_WIDTH, 160);
}
return _listHeaderView;
}
- (LSCircleListSectionHeaderView *)sectionHeaderView {
if (!_sectionHeaderView) {
_sectionHeaderView = [[LSCircleListSectionHeaderView alloc] initWithViewModel:self.viewModel.sectionHeaderViewModel];
}
return _sectionHeaderView;
}
#pragma mark - delegate
#pragma mark - UITableViewDataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.viewModel.dataArray.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
LSCircleListTableCell *cell = [tableView dequeueReusableCellWithIdentifier:[NSString stringWithUTF8String:object_getClassName([LSCircleListTableCell class])] forIndexPath:indexPath];
if (self.viewModel.dataArray.count > indexPath.row) {
cell.viewModel = self.viewModel.dataArray[indexPath.row];
}
return cell;
}
#pragma mark - UITableViewDelegate
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 100;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if (self.viewModel.dataArray.count > indexPath.row) {
[self.viewModel.cellClickSubject sendNext:self.viewModel.dataArray[indexPath.row]];
}
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
return self.sectionHeaderView;
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
return 45;
}
@end
主View分為四個模塊:
ⅰ 第一個模塊 : 系統函數
每個View都會有對應的ViewModel,這樣也更易復用,這裏因為是主View,一般而言我都會使得VC和主V共用一個VM,這樣對於跳轉、數據共享等都有著極大的好處。
ⅱ 第二個模塊 : 私有函數
具體作用途中已經標註,需要註意的是這些對於不同數據的處理,是我自己寫的,邏輯上肯定沒有那麽縝密,僅供參考。
ⅲ 第三個模塊 : 懶加載
這裏沒啥好說的,就是用的MJRefresh這個第三方庫做的刷新。不過,假如你細心的話肯定會發現下面那兩個View都是用VM來配置初始化的,這個和主View的配置初始化的意義是一樣的。
ⅳ 第四個模塊 : 代理及數據源
其中使用的是自定義Cell,用ViewModel來配置,點擊事件也是和之前的VC的跳轉聯系起來了,並將VM傳過去。
3、LSCircleListModel的處理
同樣,先上代碼
//
// LSCircleListModel.h
// ZhongShui
//
// Created by 王隆帥 on 16/3/17.
// Copyright ? 2016年 王隆帥. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface LSCircleListModel : NSObject
@property (nonatomic, copy) NSString *idStr;
@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSString *intro;
@property (nonatomic, copy) NSString *img;
@property (nonatomic, copy) NSString *memberCount;
@property (nonatomic, copy) NSString *topicCount;
@end
//
// LSCircleListModel.m
// ZhongShui
//
// Created by 王隆帥 on 16/3/17.
// Copyright ? 2016年 王隆帥. All rights reserved.
//
#import "LSCircleListModel.h"
@implementation LSCircleListModel
+ (NSDictionary *)mj_replacedKeyFromPropertyName {
return @{
@"idStr":@"id",
@"title":@"title",
@"intro":@"intro",
@"img":@"img",
@"memberCount":@"MemberCount",
@"topicCount":@"TopicCount",
};
}
@end
這個就不貼圖介紹了,就是單純的數據模型,使用了MJExtention這個數據模型轉換框架。沒有做任何其他的邏輯處理。
4、ViewModel的處理
//
// LSCircleListViewModel.h
// ZhongShui
//
// Created by 王隆帥 on 16/3/10.
// Copyright ? 2016年 王隆帥. All rights reserved.
//
#import "YCViewModel.h"
#import "LSCircleListHeaderViewModel.h"
#import "LSCircleListSectionHeaderViewModel.h"
@interface LSCircleListViewModel : YCViewModel
@property (nonatomic, strong) RACSubject *refreshEndSubject;
@property (nonatomic, strong) RACSubject *refreshUI;
@property (nonatomic, strong) RACCommand *refreshDataCommand;
@property (nonatomic, strong) RACCommand *nextPageCommand;
@property (nonatomic, strong) LSCircleListHeaderViewModel *listHeaderViewModel;
@property (nonatomic, strong) LSCircleListSectionHeaderViewModel *sectionHeaderViewModel;
@property (nonatomic, strong) NSArray *dataArray;
@property (nonatomic, strong) RACSubject *cellClickSubject;
@end
//
// LSCircleListViewModel.m
// ZhongShui
//
// Created by 王隆帥 on 16/3/10.
// Copyright ? 2016年 王隆帥. All rights reserved.
//
#import "LSCircleListViewModel.h"
#import "LSCircleListCollectionCellViewModel.h"
#import "LSCircleListModel.h"
@interface LSCircleListViewModel ()
@property (nonatomic, assign) NSInteger currentPage;
@end
@implementation LSCircleListViewModel
- (void)yc_initialize {
@weakify(self);
[self.refreshDataCommand.executionSignals.switchToLatest subscribeNext:^(NSDictionary *dict) {
@strongify(self);
if (dict == nil) {
[self.refreshEndSubject sendNext:@(LSRefreshError)];
ShowErrorStatus(@"網絡連接失敗");
return;
}
if ([dict[@"status"] integerValue] == 0) {
self.listHeaderViewModel.dataArray = [[[([(NSDictionary *)dict[@"res"] arrayForKey:@"JoinCircles"]).rac_sequence map:^id(NSDictionary *dic) {
LSCircleListModel *model = [LSCircleListModel mj_objectWithKeyValues:dic];
LSCircleListCollectionCellViewModel *viewModel = [[LSCircleListCollectionCellViewModel alloc] init];
viewModel.model = model;
return viewModel;
}] array] mutableCopy];
self.dataArray = [[[([(NSDictionary *)dict[@"res"] arrayForKey:@"Circles"]).rac_sequence map:^id(NSDictionary *dic) {
LSCircleListModel *model = [LSCircleListModel mj_objectWithKeyValues:dic];
LSCircleListCollectionCellViewModel *viewModel = [[LSCircleListCollectionCellViewModel alloc] init];
viewModel.model = model;
return viewModel;
}] array] mutableCopy];
[self ls_setHeaderRefreshWithArray:dict[@"Circles"]];
[self ls_dismiss];
} else {
[self.refreshEndSubject sendNext:@(LSRefreshError)];
ShowMessage(dict[@"mes"]);
}
}];
[[[self.refreshDataCommand.executing skip:1] take:1] subscribeNext:^(id x) {
@strongify(self);
if ([x isEqualToNumber:@(YES)]) {
[self ls_showWithStatus:@"正在加載"];
}
}];
[self.nextPageCommand.executionSignals.switchToLatest subscribeNext:^(NSDictionary *dict) {
@strongify(self);
if (dict == nil) {
[self.refreshEndSubject sendNext:@(LSRefreshError)];
ShowErrorStatus(@"網絡連接失敗");
return;
}
if ([dict[@"status"] integerValue] == 0) {
NSMutableArray *recommandArray = [[NSMutableArray alloc] initWithArray:self.dataArray];
for (NSDictionary *subDic in [(NSDictionary *)dict[@"res"] arrayForKey:@"Circles"]) {
LSCircleListModel *model = [LSCircleListModel mj_objectWithKeyValues:subDic];
LSCircleListCollectionCellViewModel *viewModel = [[LSCircleListCollectionCellViewModel alloc] init];
viewModel.model = model;
[recommandArray addObject:viewModel];
}
self.dataArray = recommandArray;
[self ls_setFootRefreshWithArray:dict[@"Circles"]];
[self ls_dismiss];
} else {
[self.refreshEndSubject sendNext:@(LSRefreshError)];
ShowMessage(dict[@"mes"]);
}
}];
}
#pragma mark - private
- (NSMutableDictionary *)requestCircleListWithId:(NSString *)idStr currentPage:(NSString *)currentPage {
idStr = IF_NULL_TO_STRING(idStr);
currentPage = IF_NULL_TO_STRING(currentPage);
NSMutableDictionary * dict = [@{@"MemberID": idStr, @"pageSize": LS_REQUEST_LIST_COUNT, @"pageIndex":currentPage} mutableCopy];
return dict;
}
- (void)ls_setFootRefreshWithArray:(NSArray *)array {
if (array.count < LS_REQUEST_LIST_NUM_COUNT) {
[self.refreshEndSubject sendNext:@(LSFooterRefresh_HasNoMoreData)];
} else {
[self.refreshEndSubject sendNext:@(LSFooterRefresh_HasMoreData)];
}
}
- (void)ls_setHeaderRefreshWithArray:(NSArray *)array {
if (array.count < LS_REQUEST_LIST_NUM_COUNT) {
[self.refreshEndSubject sendNext:@(LSHeaderRefresh_HasNoMoreData)];
} else {
[self.refreshEndSubject sendNext:@(LSHeaderRefresh_HasMoreData)];
}
}
#pragma mark - lazyLoad
- (RACSubject *)refreshUI {
if (!_refreshUI) {
_refreshUI = [RACSubject subject];
}
return _refreshUI;
}
- (RACSubject *)refreshEndSubject {
if (!_refreshEndSubject) {
_refreshEndSubject = [RACSubject subject];
}
return _refreshEndSubject;
}
- (RACCommand *)refreshDataCommand {
if (!_refreshDataCommand) {
@weakify(self);
_refreshDataCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
@strongify(self);
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
@strongify(self);
self.currentPage = 1;
[self.request POST:LS_URL_CIRCLE_MEMBER_LIST parameters:[self requestCircleListWithId:@"1" currentPage:[NSString stringWithFormat:@"%d",self.currentPage]] success:^(CMRequest *request, NSString *responseString) {
NSDictionary *dict = [responseString objectFromJSONString];
[subscriber sendNext:dict];
[subscriber sendCompleted];
} failure:^(CMRequest *request, NSError *error) {
ShowErrorStatus(@"網絡連接失敗");
[subscriber sendCompleted];
}];
return nil;
}];
}];
}
return _refreshDataCommand;
}
- (RACCommand *)nextPageCommand {
if (!_nextPageCommand) {
@weakify(self);
_nextPageCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
@strongify(self);
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
@strongify(self);
self.currentPage ++;
[self.request POST:LS_URL_CIRCLE_TOPIC_LIST parameters:nil success:^(CMRequest *request, NSString *responseString) {
NSDictionary *dict = [responseString objectFromJSONString];
[subscriber sendNext:dict];
[subscriber sendCompleted];
} failure:^(CMRequest *request, NSError *error) {
@strongify(self);
self.currentPage --;
ShowErrorStatus(@"網絡連接失敗");
[subscriber sendCompleted];
}];
return nil;
}];
}];
}
return _nextPageCommand;
}
- (LSCircleListHeaderViewModel *)listHeaderViewModel {
if (!_listHeaderViewModel) {
_listHeaderViewModel = [[LSCircleListHeaderViewModel alloc] init];
_listHeaderViewModel.title = @"已加入的圈子";
_listHeaderViewModel.cellClickSubject = self.cellClickSubject;
}
return _listHeaderViewModel;
}
- (LSCircleListSectionHeaderViewModel *)sectionHeaderViewModel {
if (!_sectionHeaderViewModel) {
_sectionHeaderViewModel = [[LSCircleListSectionHeaderViewModel alloc] init];
_sectionHeaderViewModel.title = @"推薦圈子";
}
return _sectionHeaderViewModel;
}
- (NSArray *)dataArray {
if (!_dataArray) {
_dataArray = [[NSArray alloc] init];
}
return _dataArray;
}
- (RACSubject *)cellClickSubject {
if (!_cellClickSubject) {
_cellClickSubject = [RACSubject subject];
}
return _cellClickSubject;
}
@end
ViewModel也是分為三個模塊,由於代碼太多摘重要的講
ⅰ 第一個模塊 : 處理數據、邏輯模塊
處理數據這塊,先用字典轉為Model,在用Model配置ViewModel,ViewModel再去與UI及其邏輯對應。
ⅱ 第二個模塊 : 私有函數
對於請求參數字典,可以放在VM中,便於模塊化移植,也可以放在公共API中便於管理,看個人選擇了,沒有絕對的好位置,只有更適合個人的位置。
另外兩個函數就是處理下拉及上拉時有沒有更多數據的私有函數。
ⅲ 第三個模塊 : 懶加載
此數據請求用的是AFNetworking。
5、APPDelegate的代碼簡化
一般而言,我們正式項目中會遇到很多需要啟動項目時就加載的,所以很快APPDelegate就會越來越龐大,既然其他的代碼都簡化解耦了,這裏也可以做一下處理。
目錄如下:
簡化後的AppDelegate如下:
其他代碼存放的位置如下:
當類對象被引入項目時, runtime 會向每一個類對象發送 load 消息. load 方法還是非常的神奇的, 因為它會在每一個類甚至分類被引入時僅調用一次, 調用的順序是父類優先於子類, 子類優先於分類. 而且 load 方法不會被類自動繼承, 每一個類中的 load 方法都不需要像 viewDidLoad 方法一樣調用父類的方法。
這是利用了這個算是黑魔法的玩意,哈哈,就簡化了APPDelegate!
四、後記
當初本來想幹掉基類來著,想利用Category + Protocol並利用Runtime的Methode Swizzle 來給系統函數添加自己的私有函數,當初VC已經搞定了,然而發現這樣牽涉面太廣,你對VC做了Category,UINavigationController 也會受到影響,假如你對View做了Category,其他繼承View的也會有影響,而且當時交換方法都是在一個Category裏管事,到第二個就覆蓋了。。。不造為啥,因為知道這條路走不通就沒繼續搞下去了。。。
07.04 更新
針對本小節中幹掉基類的做法(Runtime + category),已經有所實現,並按照自己的思路新寫了個列表實現,具體可查看對於iOS架構模式之爭的一些思考。
寫到這裏,大家應該都對我筆下的架構模式有了一些了解,因為裏面涉及的東西確實太多,主要是這些玩意需要站在巨人的肩膀,遇到文中沒有提到而且不懂得可以:
哈哈哈!別怪我...不是我不負責,因為你可以看看寫到這裏篇幅已經超出常人所能接受的了,而且我感覺我把各個細節已經都照顧到了吧(?乛?乛? 磨人的小妖精)!大家有什麽疑惑我們可以在評論區交流!
最後,真的很希望各位大神指出不足的地方,能讓大家共同進步!
五、Demo已出(動動你的手指,贊一下撒!)
代碼地址:https://github.com/wanglongshuai/MVVM-RAC-Demo
本文由簡書作者 王隆帥 原創編寫,轉載請保留版權網址,感謝您的理解與分享,讓生活變的更美好!(有點嚇不住人,該這樣說:如需轉載請務必通知作者,否則法律責任後果自負!)
作者:王隆帥
鏈接:http://www.jianshu.com/p/3beb21d5def2
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請註明出處。
iOS MVVM+RAC