上一個項目使用到了ReactiveCocoa+MVVM+AFNetworking+FMDB框架設計,從最初的嘗試,到后來不斷思考和學習,現在對這樣一個整體設計還是有了一定了理解與心得。在此與大家分享下。
本文將不再過多的描述ReactiveCocoa、MVVM、FMDB的使用細節。關於ReactiveCocoa,我有一篇實用案例的博客:
http://www.brighttj.com/ios/ios-reactivecocoa-utility-demo.html
文章介紹的更多的是我對這個框架設計的理解,而不是具體代碼邏輯的講解。關於代碼邏輯,我會在Demo中給出詳細的注釋,本文Demo下載地址:
https://github.com/saitjr/ReactiveCocoa-MVVM-AFNetworking-FMDB.git
環境信息:
Mac OS X 10.11
Xcode 7.0.1
iOS 9.0.1
ReactiveCocoa 2.4.7
AFNetworking 2.6.1
FMDB 2.5
MJExtension 2.5.14
正文
工程目錄
先來談談工程目錄吧,如圖:
1. 【Module】+【Model】
在這個目錄中,比較核心的是【Module】與【Model】,他們組成了整個MVVM框架。
【Module】與【Model】均包含【Base】,其中有BaseModel、BaseViewModel、BaseViewController。在開發中,我還是習慣無論是否需要基類,都去寫一個。難免開發之初就考慮到,也難免之后需求會變更。
2. 【Interface】接口
這是借鑒了Java中的接口思想,目的是為了統一方法名。例如里面的SQLInterface.h文件,就是一個對數據進行CRUD操作的protocol,並且可以規定里面的方法是否必須實現。
3. 【Configuration】配置
對項目的一些基本配置,如基本宏定義、常量、通知名,亦或是Cell的identifier。宏定義中一般包含項目基本屬性,如:主色調、常用方法等。
在提供的Demo中,我將SQL語句放在了SQL.h中,是因為SQL只有一個文件在引用,其中的定義方式是:
static NSString * const selectArticleSQL = @"SELECT * FROM article";
而NotificationNames.h會在大部分文件中用到,所以使用UIKIT_EXTERN定義為了全局變量:
-----.h
UIKIT_EXTERN NSString * const LoadAllNotification; -----.m NSString * const LoadAllNotification = @"LoadAllNotification";
5. 【Category】類目
項目中沒有打包的類目,例如給三方或系統類新增的一些方法。
RAC+MVVM
RAC+MVVM在【Module】和【Model】這兩個目錄中進行實現。在這之中,MVVM是框架思想,RAC只是輔助而已。
一、MVVM
之所以采用MVVM,而不是MVC,也得益於MVVM的一大特點,就是減輕C層的負擔,畢竟以前的C層完全就是百寶箱,什么樣的代碼都寫在里面。
對於MVVM我的理解和拆分是這樣的:
1. Model與View
這一層和MVC中的Model、View含義相同。
2. ViewModel
這一層主要作用是將以前寫在ViewController中的數據處理放在ViewModel中,如:網絡請求、數據緩存、無法直接展示的數據處理(如NSNumber這類的,就在VM中處理成NSString,然后V層直接用,而不是在V層中處理)。
二、Demo中VC的設計
圖形結合源碼應該能看個大概。
三、自定義cell的設計(可延伸至自定義View的設計)
因為介紹的是VC,所以這里再單獨說一下HomePageCell這個自定義cell的設計。
因為想到實際項目中,可能會有比較復雜的cell,所以Demo中寫的是一個比較完整的設計方式(如果單單看這個Demo的話,這個自定義cell太簡單,沒必要有一個單獨的VM,有點過度設計)。
cell中的構思是,cell有一個CellVM來管理cell中要顯示的數據,CellVM來自於VC中,dataSource數組。處理方式具體是:在網絡請求完成以后,將字典->模型,然后通過模型,初始化CellVM,然后將CellVM放入dataSource數組。
Cell實現部分代碼如下:
@implementation HomePageCell - (void)awakeFromNib { [super awakeFromNib]; [self setupSignal]; } - (void)setupSignal { @weakify(self); [RACObserve(self, viewModel) subscribeNext:^(HomePageCellViewModel *viewModel) { @strongify(self); self.textLabel.text = viewModel.titleText; self.detailTextLabel.text = viewModel.authorText; }]; } @end
AFNetworking
前幾天,AFNetworking升級到了3.0。將以前基於NSURLConnection的API,全都改成了NSURLSession,關於更新的詳情,可以看AFNetworking 3.0遷移指南這篇文章。
在頁面銷毀、重新請求等情況下,需要將還在隊列中的請求取消,以免占用資源。
考慮到這一點,結合我的網絡請求在VM發送,權衡再三,在BaseViewModel的基礎上,再進行了一次封裝——RequestViewModel。這個VM有AFHTTPSessionManager類的屬性,一個該屬性的懶加載和一個在dealloc中取消請求方法。
在以前使用MVC的時候,我會對AFNetworking進行再次的封裝,這樣更像是一個MVCS的設計,目的是防止VC過重,現在把這部分代碼扔在了VM中,看起來還好,所以並沒有對AFNetworking再次封裝。關於以前的設計方式,可以看這篇文章:
FMDB
FMDB提供了一種線程安全的模式,在這之中維護這一個串行隊列。
1. 初始化
初始化方式參考了開源項目MVVMReactiveCocoa,作者采用了類目的形式給了一個單例:
@implementation FMDatabaseQueue (Extension) + (instancetype)shareInstense { static FMDatabaseQueue *queue = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ queue = [FMDatabaseQueue databaseQueueWithPath:DB_PATH]; }); return queue; } @end
2. 統一接口
關於CRUD方法的定義,借鑒了Java Interface的設計:
3. SQL語句
根據實際情況,SQL語句參數可以采用?的形式,也可以采用:keyword的形式。
INSERT INTO myTable VALUES (?, ?, ?) INSERT INTO myTable VALUES (:id, :name, :value)
關於數據庫的創建與更新,我並不建議在Bundle中包含xxx.sql這樣的文件,因為他們會在編譯后一起打包,用戶下載解壓就能看到,並不怎么安全。目前我是直接在程序中寫的SQL,不知道大家有沒有更好的方式。
4. CRUD
在網絡請求成功的時候,存入數據。存儲是一個批量的操作,建議采用事務inTransaction實現。簡單的操作采用inDatabase即可。
FMDatabaseQueue是一個串行隊列,並且inTransaction和inDatabase都是同步線程,需要注意的是不要在block中執行另一個數據庫訪問操作,防止線程死鎖。
最后
每個項目完后,都會有很多收獲,有很多東西需要整理總結。寫這篇博客的原因有兩個:
原因之一:因為我在開發過程中踩了不少坑,可能開發到中途,發現框架設計不好。框架如何設計,並沒有一個標准答案,而且設計思想,還需要在不斷實踐中得出,所以每次總結,是為了給自己看,也是為了幫到其他有同樣困擾的朋友。
原因之二:也正是因為我不知道框架到底怎么樣,所以寫出來,讓大家看看,都多多提出建議。謝謝。
參考
1. 雷純峰的開源項目MVVMReactiveCocoa,這個項目給我的啟發很大,在此謝謝作者,也為開源點贊
2. iOS大型項目開發漫談