設(shè)計模式 (四) : MVVM + RAC

一、前言

對于設(shè)計模式這個讓人又愛又恨的玩意积糯,說來其實簡單掂墓,但一千個人眼中就有一千種哈姆雷特,說他千變?nèi)f化確實是事實看成,而且當(dāng)你深入其中的時候你真的會上癮君编,并樂此不疲!

二川慌、談?wù)凪VVM和RAC

1吃嘿、MVVM淺析

到這里我就默認(rèn)你看過MVVM相關(guān)文章(畢竟相關(guān)文章已經(jīng)可以用滿天飛來形容了~(≧▽≦)/~啦啦啦l裟恕),僅僅簡要談?wù)勎覍ζ涞睦斫狻?/p>

MVC是構(gòu)建iOS App的標(biāo)準(zhǔn)模式兑燥,是蘋果推薦的一個用來組織代碼的權(quán)威范式亮瓷,市面上大部分App都是這樣構(gòu)建的,具體組建模式不細(xì)說降瞳,iOS入門者都比較了解(雖然不一定能完全去遵守)嘱支,但其幾個不能避免的問題卻是很嚴(yán)重困擾開發(fā)者比如厚重的ViewController、遺失的網(wǎng)絡(luò)邏輯(沒有屬于它的位置)挣饥、較差的可測試性等因此也就會有維護性較強除师、耦合性很低的一種新架構(gòu)MVVM (MVC 引申出得新的架構(gòu))的流行。

MVVM雖然來自微軟扔枫,但是不應(yīng)該反對它汛聚,它正式規(guī)范了正式規(guī)范了視圖和控制器緊耦合的性質(zhì),如下圖:


MVVM圖示

ViewModel: 相比較于MVC新引入的視圖模型茧吊。是視圖顯示邏輯贞岭、驗證邏輯、網(wǎng)絡(luò)請求等代碼存放的地方搓侄,唯一要注意的是,任何視圖本身的引用都不應(yīng)該放在VM中话速,換句話說就是VM中不要引入UIKit.h (對于image這個讶踪,也有人將其看做數(shù)據(jù)來處理,這就看個人想法了泊交,并不影響整體的架構(gòu))乳讥。

這樣,首先解決了VC臃腫的問題廓俭,將邏輯代碼云石、網(wǎng)絡(luò)請求等都寫入了VM中,然后又由于VM中包含了所有的展示邏輯而且不會引用V研乒,所以它是可以通過編程充分測試的汹忠。

2、RAC淺淺析

特別淺雹熬。宽菜。。本文重點是框架及實戰(zhàn)及MVVM思想竿报,RAC這玩意話說學(xué)習(xí)曲線較長铅乡,難以理解,不好上手烈菌,是因為之前學(xué)習(xí)的時候使用者阵幸、中文教程還比較少花履,所以學(xué)習(xí)運用起來比較費勁,(當(dāng)時確實廢了好大得勁挚赊,實力裝逼一把 @%&$%& )但現(xiàn)在已經(jīng)成熟的爛大街了诡壁,只要有心,好的教程一大把咬腕,能潛下心來看我寫的水文的人欢峰,拿下RAC不在話下!

ReactiveCocoa 可以說是結(jié)合了函數(shù)式編程和響應(yīng)式編程的框架涨共,也可稱其為函數(shù)響應(yīng)式編程(FRP)框架纽帖,強調(diào)一點,RAC雖然最大的優(yōu)點是提供了一個單一的举反、統(tǒng)一的方法去處理異步的行為懊直,包括delegate方法,blocks回調(diào),target-action機制,notifications和KVO.但是不要簡單的只是單純的認(rèn)為他僅僅就是減少代碼復(fù)雜度,更好的配合MVVM而已火鼻,小伙子室囊,這樣你就小看它了。

它最大的與眾不同是提供了一種新的寫代碼的思維魁索,由于RAC將Cocoa中KVO融撞、UIKit event、delegate粗蔚、selector等都增加了RAC支持尝偎,所以都不用去做很多跨函數(shù)的事。

如果全工程都使用RAC來實現(xiàn)鹏控,對于同一個業(yè)務(wù)邏輯終于可以在同一塊代碼里完成了致扯,將UI事件,邏輯處理当辐,文件或數(shù)據(jù)庫操作抖僵,異步網(wǎng)絡(luò)請求,UI結(jié)果顯示缘揪,這一大套統(tǒng)統(tǒng)用函數(shù)式編程的思路嵌套起來耍群,進入頁面時搭建好這所有的關(guān)系,用戶點擊后妥妥的等著這一套聯(lián)系一個個的按期望的邏輯和次序觸發(fā)寺晌,最后顯示給用戶世吨。

3、本篇對兩者的理解運用

在此次介紹中呻征,會使用MVVM+RAC結(jié)合的方式耘婚,搞定一個添加上拉加載及下拉刷新的列表,所以更多的詮釋MVVM思想陆赋,而不是RAC的邏輯鏈?zhǔn)讲僮鳎ㄟ@一點用登錄界面來寫更能體現(xiàn)Y^o^Y )沐祷,RAC在此扮演的更大一部分的角色是更好的解耦嚷闭,減少代碼復(fù)雜度,使代碼層次分明赖临、邏輯清晰更便于維護升級胞锰。

二、框架部分

1兢榨、框架目錄詳解

首先介紹一下本框架的目錄結(jié)構(gòu)嗅榕,如下圖


1、Frameworks

存放系統(tǒng)庫的虛擬文件夾,目前搭建框架的時候需要手動添加一個名稱為Frameworks的虛擬文件夾,這樣你在Build Phases 中添加的系統(tǒng)庫會自動歸入此文件夾吵聪,不會直接在外部顯示以至于打亂目錄結(jié)構(gòu)凌那。系統(tǒng)庫添加流程如下:



另外,細(xì)心地家伙會發(fā)現(xiàn)此目錄中有兩個相同的Frameworks, 那這到底是什么鬼?最上面的那個Frameworks是在自己搭框架自己添加的,當(dāng)時的項目還很單純, 沒有這么淘氣吟逝,問題出在下面那個Pods Target上帽蝶,添加它之后就會自動給你生成一個虛擬的Frameworks的文件夾。

既然提到了Pods块攒,那接下來講講CocoaPods(第三方類庫管理工具)励稳。

2、CocoaPods

當(dāng)你開發(fā)iOS應(yīng)用時囱井,會經(jīng)常使用到很多第三方開源類庫驹尼,比如JSONKit,AFNetWorking等等庞呕》鲂溃可能某個類庫又用到其他類庫,所以要使用它千扶,必須得另外下載其他類庫,而其他類庫又用到其他類庫骆捧,“子子孫孫無窮盡也”澎羞,手動一個個去下載所需類庫是十分麻煩的。

還有另外一種常見情況是敛苇,你項目中用到的類庫有更新妆绞,你必須得重新下載新版本,重新加入到項目中枫攀,十分麻煩括饶。

CocoaPods就是幫你解決上面的問題的,話說這玩意應(yīng)該是iOS最常用最有名的類庫管理工具了来涨,作為iOS程序員的我們图焰,掌握CocoaPods的使用是必不可少的基本技能了。

3蹦掐、AppDelegate

這個目錄下放的是AppDelegate.h(.m)文件技羔,是整個應(yīng)用的入口文件僵闯,所以單獨拿出來。一會兒告訴你如何寫一個簡潔的AppDelegate藤滥,會在這個文件夾里添加一些類鳖粟,所以將其放入一個文件夾內(nèi)還是很有必要的。

4拙绊、Class

工程主體類, 日常大部分開發(fā)代碼均在這里,又細(xì)分了好多次級目錄向图。

通用類

General :通用類(文件夾項目移植過程中都不需要更改的就能直接使用的)

Base :基類 (整個框架的基類)

Categories :公共擴展類 (就是一些常用的類別,比如分享啊什么的)

Core :公共核心類(一般存放個人信息标沪、接口API等)

Models :公共Model (公用的一些數(shù)據(jù)模型)

Views :公共View (封裝的一些常用的View)

工具類

Helpers :工程的相關(guān)輔助類(比如類似數(shù)據(jù)請求榄攀、表單上傳、網(wǎng)絡(luò)監(jiān)測等工具類)

宏定義類

Macro :宏定義類 (就是整個應(yīng)用會用到的宏定義)

AppMacro.happ項目的相關(guān)宏定義

NotificationMacro.h通知相關(guān)的宏定義

VendorMacro.h第三方相關(guān)宏定義

UtilsMacro.h為簡化代碼的宏定義

...等等等等(其他隨你定啦谨娜!Y^o^Y )

APP具體模塊代碼類

Sections :各模塊的文件夾(一般而言航攒,我們以人為單位)

LSSections

CLSections

...等等等等

每個成員的文件夾下是其所負(fù)責(zé)模塊的文件夾,如下(接著上面的個人文件夾):

PHP :模塊名趴梢,也可以是首頁(HomePage)...等等

ViewControllers界面控制器存放處(這是文件夾名)

ViewModels打雜的(MVVM的核心漠畜、解耦合、處理邏輯等)

Views界面相關(guān)View存放處(界面相關(guān)子View)

Models數(shù)據(jù)模型存放處(各種單純的數(shù)據(jù)模型坞靶,一點都不胖憔狞,是標(biāo)準(zhǔn)的瘦Model)

這就是標(biāo)準(zhǔn)的MVVM了。彰阴。瘾敢。為啥不和上面目錄連起來呢?為啥呢尿这?為啥呢簇抵?因為臣妾做不到啊I渲凇5凇!(不會三級叨橱、四級列表的MarkDown寫法典蜕,求大神支招!良辰必有重謝B尴础)

第三方類庫

Vendors :第三方的類庫/SDK愉舔,如UMeng、WeiboSDK伙菜、WeixinSDK等等轩缤。

到這哥們又該疑惑了,心里該碎碎念了:(????д????)????? What are you 弄啥嘞!剛才剛講了個第三方庫管理CocoaPods典奉,你丫這里自己又搞了一個躺翻,?( ˉ?? ˉ?) 信不信我突突了你!

哈哈哈卫玖,剛才的CocoaPods確實管理著大部分的第三方庫公你,這里建立第三方庫目錄的原因有兩個:其一,并不是所有的你需要的第三方都支持pods的假瞬,所以還是需要手動添加一些類庫陕靠。其二,一些第三方庫雖然支持pods脱茉,但是需要我們?nèi)ジ纳踔磷远x這個第三方剪芥,此時也需要放入這里,也防止使用pods一不小心更新掉你的自定義琴许!?(?)? 你來打我八胺尽!

5榜田、Resource

這里放置的是工程所需的一些資源益兄,如下

Fonts字體

Images圖片(當(dāng)然你可以添加至Assets.xcassets, 沒人攔著你)

Sounds聲音

Videos視頻

ok,目錄就講到這里箭券!想知道更詳細(xì)的可以私信我净捅!

2、基類詳解

這里著重講解一下VC辩块、V蛔六、VM的基類,其他的模式與View類似所以略過废亭,其中TableViewCell的基類稍微特殊所以也提一下国章。

我目前的基類如下圖:


1、YDViewController



由于Cell比較特殊豆村,所以單拎出來說一下捉腥。觀察上面的ViewMdoel、View等的基類會發(fā)現(xiàn)每個基類都會有數(shù)據(jù)綁定的地方你画,但是cell得數(shù)據(jù)綁定需要放在數(shù)據(jù)初始化的時候,因為所有的基類的數(shù)據(jù)邏輯綁定都是在沒有返回初始化對象的時候調(diào)用的桃漾,但是cell中假如在那里面進行數(shù)據(jù)綁定會出現(xiàn)問題比如下圖:


上圖中的函數(shù)假如是在 bindViewModel 內(nèi)坏匪,則會復(fù)用失敗,點擊按鈕是沒有反應(yīng)的撬统,但是假如是在數(shù)據(jù)初始化的時候調(diào)用:比如 setViewModel 的時候适滓,就會OK了,因為里面用到了cell的在RAC中復(fù)用機制rac_prepareForReuseSignal恋追,在cell還沒有初始化返回的時候是失效的凭迹。

3罚屋、題外話

基類的作用是統(tǒng)一管理,統(tǒng)一風(fēng)格嗅绸,便于編碼脾猛,有更多的額外的附加功能的話,建議使用Protocol 或 Category鱼鸠,這樣移植性強猛拴,便于管理與擴展,不至于牽一發(fā)而動全身蚀狰。

本篇基類核心是用VM來配置V(VC)愉昆,并提供一些必須的Protocol方法來處理界面顯示、邏輯麻蹋,將代碼風(fēng)格規(guī)范化跛溉,各個部分的功能明朗化,這樣扮授,當(dāng)你需要寫什么芳室,需要找什么,需要更改什么的時候都會很明確這些代碼的位置糙箍,邏輯更清晰渤愁,而不會浪費更多的時間在思考應(yīng)該寫在哪,該去哪找深夯,要改的地方在哪這種不該費時間的問題上抖格。

三、實戰(zhàn)部分(經(jīng)典列表的實現(xiàn))

這里講一下如下界面的代碼構(gòu)造方式咕晋,很普通的一個列表:(懶得再寫了雹拄,這是我之前做的一個項目的一個界面,之前基類講解中會看到都是YD開頭的掌呜,在這里是YC開頭就這個區(qū)別而已)


首先觀察這個界面滓玖,需求是:頭部的內(nèi)容數(shù)量多的話是可以左右滑動的,然后整體是可以上拉加載的质蕉。我是這樣處理的:首先界面整體是一個TableView势篡,然后分為一個Header、一個Section和主體列表Row模暗。在Header上嵌套一個CollectionView保證可復(fù)用禁悠。具體分層如下


然后處理完后的目錄如下:



簡單介紹一下:

ViewController

LSCircleListViewController :界面主控制器,負(fù)責(zé)跳轉(zhuǎn)兑宇、Navgation碍侦、TabBar等

View

LSCircleListView :界面主View,負(fù)責(zé)主要界面的顯示

LSCircleListHeaderView :頭部Header,封裝的內(nèi)部含有一個CollectionView

LSCircleListCollectionCell :頭部Header中的CollectionView自定義的Cell

LSCircleListSectionHeaderView :SectionView瓷产,此界面不需復(fù)用站玄,所以單純一個View即可,若需要復(fù)用需要TableViewHeaderFooterView

LSCircleListTableCell :主TableView的Cell

ViewModel

LSCircleListViewModel :界面主ViewModel

LSCircleListHeaderViewModel :頭部Header對應(yīng)的ViewModel

LSCircleListCollectionCellViewModel :頭部CollectionCell及TableViewCell的ViewModel(因為二者的數(shù)據(jù)結(jié)構(gòu)是一致的)

LSCircleListSectionHeaderViewModel :Section的ViewModel

Model

LSCircleListModel :圈子的數(shù)據(jù)模型(header和tableViewCell數(shù)據(jù)結(jié)構(gòu)是一致的)

一個小小的界面這么多類...是不是難以接受了濒旦,淡定些株旷,騷年!你要想想把這些個東西都放在VC內(nèi)是個什么趕腳疤估?也得好幾千行呢T殖!(有點夸張!不過也夠頭疼的)铃拇,這么多類钞瀑,這里著重講一下主VC、主V慷荔、主VM雕什、主M就ok,能詳細(xì)講明白MVVM之間是如何工作的就一通百通了显晶。

1贷岸、LSCircleListViewController的處理

先上代碼:

#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 第一個模塊:系統(tǒng)函數(shù)


此函數(shù)是從iOS6.0開始在ViewController中新增一個更新約束布局的方法偿警,這個方法默認(rèn)的實現(xiàn)是調(diào)用對應(yīng)View的 updateConstraints 移国。ViewController的View在更新視圖布局時酥诽,會先調(diào)用ViewController的updateViewConstraints 方法吸祟。我們可以通過重寫這個方法去更新當(dāng)前View的內(nèi)部布局擒贸,而不用再繼承這個View去重寫-updateConstraints方法。我們在重寫這個方法時誉结,務(wù)必要調(diào)用 super 或者 調(diào)用當(dāng)前View的 -updateConstraints 方法董虱。

ⅱ 第二個模塊 : 私有函數(shù)


前面基類內(nèi)也提到了這三個函數(shù)的具體作用店煞,即

yd_addSubviews :添加View到ViewController

yd_bindViewModel :這里綁定了兩個跳轉(zhuǎn)事件苞慢。

yd_layoutNavigation :設(shè)置了標(biāo)題為“圈子列表”诵原、及TabBar不隱藏

ⅲ 第三個模塊 : 懶加載


這就不用解釋了,用到時再加載挽放。

2绍赛、View的處理

先上代碼

#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分為四個模塊:

ⅰ 第一個模塊 : 系統(tǒng)函數(shù)



每個View都會有對應(yīng)的ViewModel,這樣也更易復(fù)用辑畦,這里因為是主View惹资,一般而言我都會使得VC和主V共用一個VM,這樣對于跳轉(zhuǎn)航闺、數(shù)據(jù)共享等都有著極大的好處。

ⅱ 第二個模塊 : 私有函數(shù)




具體作用途中已經(jīng)標(biāo)注,需要注意的是這些對于不同數(shù)據(jù)的處理潦刃,是我自己寫的侮措,邏輯上肯定沒有那么縝密,僅供參考乖杠。

ⅲ 第三個模塊 : 懶加載


這里沒啥好說的分扎,就是用的MJRefresh這個第三方庫做的刷新。不過胧洒,假如你細(xì)心的話肯定會發(fā)現(xiàn)下面那兩個View都是用VM來配置初始化的畏吓,這個和主View的配置初始化的意義是一樣的。

ⅳ 第四個模塊 : 代理及數(shù)據(jù)源



其中使用的是自定義Cell卫漫,用ViewModel來配置菲饼,點擊事件也是和之前的VC的跳轉(zhuǎn)聯(lián)系起來了,并將VM傳過去列赎。

3宏悦、LSCircleListModel的處理

同樣,先上代碼

#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

#import "LSCircleListModel.h"

@implementation LSCircleListModel

+ (NSDictionary *)mj_replacedKeyFromPropertyName {

??? return? @{

????????????? @"idStr":@"id",

????????????? @"title":@"title",

????????????? @"intro":@"intro",

????????????? @"img":@"img",

????????????? @"memberCount":@"MemberCount",

????????????? @"topicCount":@"TopicCount",

????????????? };

}

@end

這個就不貼圖介紹了包吝,就是單純的數(shù)據(jù)模型饼煞,使用了MJExtention這個數(shù)據(jù)模型轉(zhuǎn)換框架。沒有做任何其他的邏輯處理诗越。

4砖瞧、ViewModel的處理

#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

#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(@"網(wǎng)絡(luò)連接失敗");

??????????? 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(@"網(wǎng)絡(luò)連接失敗");

??????????? 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(@"網(wǎng)絡(luò)連接失敗");

??????????????????? [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(@"網(wǎng)絡(luò)連接失敗");

??????????????????? [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也是分為三個模塊,由于代碼太多摘重要的講

ⅰ 第一個模塊 : 處理數(shù)據(jù)嚷狞、邏輯模塊



處理數(shù)據(jù)這塊块促,先用字典轉(zhuǎn)為Model,在用Model配置ViewModel感耙,ViewModel再去與UI及其邏輯對應(yīng)褂乍。

ⅱ 第二個模塊 : 私有函數(shù)



對于請求參數(shù)字典,可以放在VM中即硼,便于模塊化移植逃片,也可以放在公共API中便于管理,看個人選擇了只酥,沒有絕對的好位置褥实,只有更適合個人的位置。

另外兩個函數(shù)就是處理下拉及上拉時有沒有更多數(shù)據(jù)的私有函數(shù)裂允。

ⅲ 第三個模塊 : 懶加載


此數(shù)據(jù)請求用的是AFNetworking损离。

5、APPDelegate的代碼簡化

一般而言绝编,我們正式項目中會遇到很多需要啟動項目時就加載的僻澎,所以很快APPDelegate就會越來越龐大貌踏,既然其他的代碼都簡化解耦了,這里也可以做一下處理窟勃。

目錄如下:


簡化后的AppDelegate如下:


其他代碼存放的位置如下:



當(dāng)類對象被引入項目時, runtime 會向每一個類對象發(fā)送 load 消息. load 方法還是非常的神奇的, 因為它會在每一個類甚至分類被引入時僅調(diào)用一次, 調(diào)用的順序是父類優(yōu)先于子類, 子類優(yōu)先于分類. 而且 load 方法不會被類自動繼承, 每一個類中的 load 方法都不需要像 viewDidLoad 方法一樣調(diào)用父類的方法祖乳。

這是利用了這個算是黑魔法的玩意,哈哈秉氧,就簡化了APPDelegate眷昆!

四、后記

當(dāng)初本來想干掉基類來著汁咏,想利用Category + Protocol并利用Runtime的Methode Swizzle 來給系統(tǒng)函數(shù)添加自己的私有函數(shù)亚斋,當(dāng)初VC已經(jīng)搞定了,然而發(fā)現(xiàn)這樣牽涉面太廣攘滩,你對VC做了Category帅刊,UINavigationController 也會受到影響,假如你對View做了Category轰驳,其他繼承View的也會有影響厚掷,而且當(dāng)時交換方法都是在一個Category里管事,到第二個就覆蓋了级解。冒黑。。不造為啥勤哗,因為知道這條路走不通就沒繼續(xù)搞下去了抡爹。。芒划。

寫到這里冬竟,大家應(yīng)該都對我筆下的設(shè)計模式有了一些了解,因為里面涉及的東西確實太多民逼,主要是這些玩意需要站在巨人的肩膀泵殴,遇到文中沒有提到而且不懂得可以:


?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市拼苍,隨后出現(xiàn)的幾起案子笑诅,更是在濱河造成了極大的恐慌,老刑警劉巖疮鲫,帶你破解...
    沈念sama閱讀 211,194評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件吆你,死亡現(xiàn)場離奇詭異,居然都是意外死亡俊犯,警方通過查閱死者的電腦和手機妇多,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評論 2 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來燕侠,“玉大人者祖,你說我怎么就攤上這事立莉。” “怎么了七问?”我有些...
    開封第一講書人閱讀 156,780評論 0 346
  • 文/不壞的土叔 我叫張陵桃序,是天一觀的道長。 經(jīng)常有香客問我烂瘫,道長,這世上最難降的妖魔是什么奇适? 我笑而不...
    開封第一講書人閱讀 56,388評論 1 283
  • 正文 為了忘掉前任坟比,我火速辦了婚禮,結(jié)果婚禮上嚷往,老公的妹妹穿的比我還像新娘葛账。我一直安慰自己,他們只是感情好皮仁,可當(dāng)我...
    茶點故事閱讀 65,430評論 5 384
  • 文/花漫 我一把揭開白布籍琳。 她就那樣靜靜地躺著,像睡著了一般贷祈。 火紅的嫁衣襯著肌膚如雪趋急。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,764評論 1 290
  • 那天势誊,我揣著相機與錄音呜达,去河邊找鬼。 笑死粟耻,一個胖子當(dāng)著我的面吹牛查近,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播挤忙,決...
    沈念sama閱讀 38,907評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼霜威,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了册烈?” 一聲冷哼從身側(cè)響起戈泼,我...
    開封第一講書人閱讀 37,679評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎茄厘,沒想到半個月后矮冬,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,122評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡次哈,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,459評論 2 325
  • 正文 我和宋清朗相戀三年胎署,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片窑滞。...
    茶點故事閱讀 38,605評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡琼牧,死狀恐怖恢筝,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情巨坊,我是刑警寧澤撬槽,帶...
    沈念sama閱讀 34,270評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站趾撵,受9級特大地震影響侄柔,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜占调,卻給世界環(huán)境...
    茶點故事閱讀 39,867評論 3 312
  • 文/蒙蒙 一暂题、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧究珊,春花似錦薪者、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至取试,卻和暖如春悬槽,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背想括。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評論 1 265
  • 我被黑心中介騙來泰國打工陷谱, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人瑟蜈。 一個月前我還...
    沈念sama閱讀 46,297評論 2 360
  • 正文 我出身青樓烟逊,卻偏偏與公主長得像,于是被迫代替她去往敵國和親铺根。 傳聞我的和親對象是個殘疾皇子宪躯,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,472評論 2 348

推薦閱讀更多精彩內(nèi)容