iOS MVVM+RAC 從框架到實戰

王隆帥發表於2016-07-20

一、前言

很早之前就想寫寫自己在設計模式方面的心得,但是一直感覺自己是井底之蛙,畢竟在iOS領域越深入越感到自己的無知,心中有著敬畏之心,就更沒有自信去寫這個東西(你也可以理解是沒時間(>﹏<),請原諒我的裝逼,嘿嘿).

對於設計模式這個讓人又愛又恨的玩意,說來其實簡單,但一千個人眼中就有一千種哈姆雷特,說他千變萬化確實是事實,而且當你深入其中的時候你真的會上癮,並樂此不疲

前幾天自己寫的一篇《iOS Xcode全面剖析》閱讀量在短短一天內破千,還上了簡書首頁(你看這句話字型就知道不是廣告了( ⊙o⊙ )),確實很開心,昨天又跟我一朋友用程式碼講解了我對MVVM的理解及運用,此情此景下,腦袋一熱搞出一篇來分享給大家也情有可原,當然更希望有更多的大神來指點一下,讓我自己也讓大家有提升就夠了,萬分感謝!

二、談談MVVM和RAC

1、MVVM淺析

到這裡我就預設你看過MVVM相關文章(畢竟相關文章已經可以用滿天飛來形容了~(≧▽≦)/~啦啦啦!),僅僅簡要談談我對其的理解。

MVC是構建iOS App的標準模式,是蘋果推薦的一個用來組織程式碼的權威正規化,市面上大部分App都是這樣構建的,具體組建模式不細說,iOS入門者都比較瞭解(雖然不一定能完全去遵守),但其幾個不能避免的問題卻是很嚴重困擾開發者比如厚重的ViewController、遺失的網路邏輯(沒有屬於它的位置)、較差的可測試性等因此也就會有維護性較強、耦合性很低的一種新架構MVVM (MVC 引申出得新的架構)的流行。

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 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 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中便於管理,看個人選擇了,沒有絕對的好位置,只有更適合個人的位置。

另外兩個函式就是處理下拉及上拉時有沒有更多資料的私有函式。

ⅲ 第三個模組 : 懶載入

此資料請求用的是AFN再度封裝的一個工具類,實際上就是AFN。

5、APPDelegate的程式碼簡化

一般而言,我們正式專案中會遇到很多需要啟動專案時就載入的,所以很快APPDelegate就會越來越龐大,既然其他的程式碼都簡化解耦了,這裡也可以做一下處理。

目錄如下:

簡化後的AppDelegate如下:

其他程式碼存放的位置如下:

當類物件被引入專案時, runtime 會向每一個類物件傳送 load 訊息. load 方法還是非常的神奇的, 因為它會在每一個類甚至分類被引入時僅呼叫一次, 呼叫的順序是父類優先於子類, 子類優先於分類. 而且 load 方法不會被類自動繼承, 每一個類中的 load 方法都不需要像 viewDidLoad 方法一樣呼叫父類的方法。

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

四、後記

當初本來想幹掉基類來著,想利用Category + Protocol並利用Runtime的Methode Swizzle 來給系統函式新增自己的私有函式,當初VC已經搞定了,然而發現這樣牽涉面太廣,你對VC做了Category,UINavigationController 也會受到影響,假如你對View做了Category,其他繼承View的也會有影響,而且當時交換方法都是在一個Category裡管事,到第二個就覆蓋了。。。不造為啥,因為知道這條路走不通就沒繼續搞下去了。。。

寫到這裡,大家應該都對我筆下的設計模式有了一些瞭解,因為裡面涉及的東西確實太多,主要是這些玩意需要站在巨人的肩膀,遇到文中沒有提到而且不懂得可以:

哈哈哈!別怪我...不是我不負責,因為你可以看看寫到這裡篇幅已經超出常人所能接受的了,而且我感覺我把各個細節已經都照顧到了吧(๑乛◡乛๑ 磨人的小妖精)!大家有什麼疑惑我們可以在評論區交流!

最後,真的很希望各位大神指出不足的地方,能讓大家共同進步!

本文由作者 王隆帥 編寫,轉載請保留版權網址,感謝您的理解與分享,讓生活變的更美好!

相關文章