MVVM 是一種軟件架構(gòu)模式歉眷,它是 Martin Fowler 的 Presentation Model 的一種變體,最先由微軟的架構(gòu)師 John Gossman 在 2005 年提出,并應(yīng)用在微軟的 WPF 和 Silverlight 軟件開發(fā)中中狂。MVVM
衍生于 MVC 圈匆,是對(duì) MVC
的一種演進(jìn)晤斩,它促進(jìn)了 UI
代碼與業(yè)務(wù)邏輯的分離。
說明:本文將采用理論與實(shí)踐相結(jié)合的方式坚俗,重點(diǎn)介紹一個(gè)使用 MVVM
和 RAC
開發(fā)的 iOS
開源項(xiàng)目 MVVMReactiveCocoa 镜盯,目的是希望能為你實(shí)踐 MVVM
提供幫助。不過猖败,在正式開始介紹正文之前速缆,請(qǐng)你先思考以下三個(gè)問題:
-
MVC
與MVVM
有什么異同點(diǎn),MVC
到MVVM
是怎樣演進(jìn)的恩闻; -
RAC
在MVVM
中扮演什么樣的角色艺糜,MVVM
是否一定要結(jié)合RAC
使用; - 如何將一個(gè)現(xiàn)有的
MVC
應(yīng)用轉(zhuǎn)變成一個(gè)MVVM
應(yīng)用幢尚,有哪些需要注意的地方破停。
帶著以上問題,我們一起進(jìn)入正文尉剩。
名詞解釋:本文中的 RAC
為 ReactiveCocoa
的縮寫真慢。
MVC
MVC
是 iOS
開發(fā)中使用最普遍的架構(gòu)模式,同時(shí)也是蘋果官方推薦的架構(gòu)模式理茎。MVC
代表的是 Model–view–controller
黑界,它們之間的關(guān)系如下:
是的管嬉,MVC
看上去棒極了,model
代表數(shù)據(jù)朗鸠,view
代表 UI
蚯撩,而 controller
則負(fù)責(zé)協(xié)調(diào)它們兩者之間的關(guān)系。然而烛占,盡管從技術(shù)上看 view
和 controller
是相互獨(dú)立的胎挎,但事實(shí)上它們幾乎總是結(jié)對(duì)出現(xiàn),一個(gè) view
只能與一個(gè) controller
進(jìn)行匹配忆家,反之亦然犹菇。既然如此,那我們?yōu)楹尾粚⑺鼈兛醋饕粋€(gè)整體呢:
因此弦赖,M-VC
可能是對(duì) iOS
中的 MVC
模式更為準(zhǔn)確的解讀项栏。在一個(gè)典型的 MVC
應(yīng)用中浦辨,controller
由于承載了過多的邏輯蹬竖,往往會(huì)變得臃腫不堪,所以 MVC
也經(jīng)常被人調(diào)侃成 Massive View Controller :
iOS architecture, where MVC stands for Massive View Controller.
坦白說流酬,有一部分邏輯確實(shí)是屬于 controller
的币厕,但是也有一部分邏輯是不應(yīng)該被放置在 controller
中的。比如芽腾,將 model
中的 NSDate
轉(zhuǎn)換成 view
可以展示的 NSString
等旦装。在 MVVM
中,我們將這些邏輯統(tǒng)稱為展示邏輯摊滔。
MVVM
因此阴绢,一種可以很好地解決 Massive View Controller
問題的辦法就是將 controller
中的展示邏輯抽取出來,放置到一個(gè)專門的地方艰躺,而這個(gè)地方就是 viewModel
呻袭。其實(shí),我們只要在上圖中的 M-VC
之間放入 VM
腺兴,就可以得到 MVVM
模式的結(jié)構(gòu)圖:
從上圖中左电,我們可以非常清楚地看到 MVVM
中四個(gè)組件之間的關(guān)系。注:除了 view
页响、viewModel
和 model
之外篓足,MVVM
中還有一個(gè)非常重要的隱含組件 binder
:
-
view
:由MVC
中的view
和controller
組成,負(fù)責(zé)UI
的展示闰蚕,綁定viewModel
中的屬性栈拖,觸發(fā)viewModel
中的命令; -
viewModel
:從MVC
的controller
中抽取出來的展示邏輯没陡,負(fù)責(zé)從model
中獲取view
所需的數(shù)據(jù)涩哟,轉(zhuǎn)換成view
可以展示的數(shù)據(jù)烟瞧,并暴露公開的屬性和命令供view
進(jìn)行綁定; -
model
:與MVC
中的model
一致染簇,包括數(shù)據(jù)模型参滴、訪問數(shù)據(jù)庫(kù)的操作和網(wǎng)絡(luò)請(qǐng)求等; -
binder
:在MVVM
中锻弓,聲明式的數(shù)據(jù)和命令綁定是一個(gè)隱含的約定砾赔,它可以讓開發(fā)者非常方便地實(shí)現(xiàn)view
和viewModel
的同步,避免編寫大量繁雜的樣板化代碼青灼。在微軟的MVVM
實(shí)現(xiàn)中暴心,使用的是一種被稱為 XAML 的標(biāo)記語(yǔ)言。
ReactiveCocoa
盡管杂拨,在 iOS
開發(fā)中专普,系統(tǒng)并沒有提供類似的框架可以讓我們方便地實(shí)現(xiàn) binder
功能,不過弹沽,值得慶幸的是檀夹,GitHub
開源的 RAC
,給了我們一個(gè)非常不錯(cuò)的選擇策橘。
RAC
是一個(gè) iOS
中的函數(shù)式響應(yīng)式編程框架炸渡,它受 Functional Reactive Programming 的啟發(fā),是 Justin Spahr-Summers 和 Josh Abernathy 在開發(fā) GitHub for Mac 過程中的一個(gè)副產(chǎn)品丽已,它提供了一系列用來組合和轉(zhuǎn)換值流的 API
蚌堵。如需了解更多關(guān)于 RAC
的信息,可以閱讀我的上一篇文章《ReactiveCocoa v2.5 源碼解析之架構(gòu)總覽》沛婴。
在 iOS
的 MVVM
實(shí)現(xiàn)中吼畏,我們可以使用 RAC
來在 view
和 viewModel
之間充當(dāng) binder
的角色,優(yōu)雅地實(shí)現(xiàn)兩者之間的同步嘁灯。此外泻蚊,我們還可以把 RAC
用在 model
層,使用 Signal
來代表異步的數(shù)據(jù)獲取操作旁仿,比如讀取文件藕夫、訪問數(shù)據(jù)庫(kù)和網(wǎng)絡(luò)請(qǐng)求等。說明枯冈,RAC
的后一個(gè)應(yīng)用場(chǎng)景是與 MVVM
無關(guān)的毅贮,也就是說,我們同樣可以在 MVC
的 model
層這么用尘奏。
小結(jié)
綜上所述滩褥,我們只要將 MVC
中的 controller
中的展示邏輯抽取出來,放置到 viewModel
中炫加,然后通過一定的技術(shù)手段瑰煎,比如 RAC
來同步 view
和 viewModel
铺然,就完成了 MVC
到 MVVM
的轉(zhuǎn)變。
Talk is cheap. Show me the code.
下面酒甸,我們直接上代碼魄健,一起來看一個(gè) MVC
模式轉(zhuǎn)換成 MVVM
模式的示例。首先是 model
層的代碼 Person
:
@interface Person : NSObject
- (instancetype)initwithSalutation:(NSString *)salutation firstName:(NSString *)firstName lastName:(NSString *)lastName birthdate:(NSDate *)birthdate;
@property (nonatomic, copy, readonly) NSString *salutation;
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, copy, readonly) NSDate *birthdate;
@end
然后是 view
層的代碼 PersonViewController
插勤,在 viewDidLoad
方法中沽瘦,我們將 Person
中的屬性進(jìn)行一定的轉(zhuǎn)換后,賦值給相應(yīng)的 view
進(jìn)行展示:
- (void)viewDidLoad {
[super viewDidLoad];
if (self.model.salutation.length > 0) {
self.nameLabel.text = [NSString stringWithFormat:@"%@ %@ %@", self.model.salutation, self.model.firstName, self.model.lastName];
} else {
self.nameLabel.text = [NSString stringWithFormat:@"%@ %@", self.model.firstName, self.model.lastName];
}
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"EEEE MMMM d, yyyy"];
self.birthdateLabel.text = [dateFormatter stringFromDate:model.birthdate];
}
接下來农尖,我們引入一個(gè) viewModel
析恋,將 PersonViewController
中的展示邏輯抽取到這個(gè) PersonViewModel
中:
@interface PersonViewModel : NSObject
- (instancetype)initWithPerson:(Person *)person;
@property (nonatomic, strong, readonly) Person *person;
@property (nonatomic, copy, readonly) NSString *nameText;
@property (nonatomic, copy, readonly) NSString *birthdateText;
@end
@implementation PersonViewModel
- (instancetype)initWithPerson:(Person *)person {
self = [super init];
if (self) {
_person = person;
if (person.salutation.length > 0) {
_nameText = [NSString stringWithFormat:@"%@ %@ %@", self.person.salutation, self.person.firstName, self.person.lastName];
} else {
_nameText = [NSString stringWithFormat:@"%@ %@", self.person.firstName, self.person.lastName];
}
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"EEEE MMMM d, yyyy"];
_birthdateText = [dateFormatter stringFromDate:person.birthdate];
}
return self;
}
@end
最終,PersonViewController
將會(huì)變得非常輕量級(jí):
- (void)viewDidLoad {
[super viewDidLoad];
self.nameLabel.text = self.viewModel.nameText;
self.birthdateLabel.text = self.viewModel.birthdateText;
}
怎么樣盛卡?其實(shí) MVVM
并沒有想像中的那么難吧助隧,而且更重要的是它也沒有破壞 MVC
的現(xiàn)有結(jié)構(gòu),只不過是移動(dòng)了一些代碼滑沧,僅此而已并村。好了,說了這么多嚎货,那 MVVM
相比 MVC
到底有哪些好處呢橘霎?我想,主要可以歸納為以下三點(diǎn):
- 由于展示邏輯被抽取到了
viewModel
中殖属,所以view
中的代碼將會(huì)變得非常輕量級(jí); - 由于
viewModel
中的代碼是與UI
無關(guān)的瓦盛,所以它具有良好的可測(cè)試性洗显; - 對(duì)于一個(gè)封裝了大量業(yè)務(wù)邏輯的
model
來說,改變它可能會(huì)比較困難原环,并且存在一定的風(fēng)險(xiǎn)挠唆。在這種場(chǎng)景下,viewModel
可以作為model
的適配器使用嘱吗,從而避免對(duì)model
進(jìn)行較大的改動(dòng)玄组。
通過前面的示例,我們對(duì)第一點(diǎn)已經(jīng)有了一定的感觸谒麦;至于第三點(diǎn)俄讹,可能對(duì)于一個(gè)復(fù)雜的大型應(yīng)用來說,才會(huì)比較明顯绕德;下面患膛,我們還是使用前面的示例,來直觀地感受下第二點(diǎn)好處:
SpecBegin(Person)
NSString *salutation = @"Dr.";
NSString *firstName = @"first";
NSString *lastName = @"last";
NSDate *birthdate = [NSDate dateWithTimeIntervalSince1970:0];
it (@"should use the salutation available. ", ^{
Person *person = [[Person alloc] initWithSalutation:salutation firstName:firstName lastName:lastName birthdate:birthdate];
PersonViewModel *viewModel = [[PersonViewModel alloc] initWithPerson:person];
expect(viewModel.nameText).to.equal(@"Dr. first last");
});
it (@"should not use an unavailable salutation. ", ^{
Person *person = [[Person alloc] initWithSalutation:nil firstName:firstName lastName:lastName birthdate:birthdate];
PersonViewModel *viewModel = [[PersonViewModel alloc] initWithPerson:person];
expect(viewModel.nameText).to.equal(@"first last");
});
it (@"should use the correct date format. ", ^{
Person *person = [[Person alloc] initWithSalutation:nil firstName:firstName lastName:lastName birthdate:birthdate];
PersonViewModel *viewModel = [[PersonViewModel alloc] initWithPerson:person];
expect(viewModel.birthdateText).to.equal(@"Thursday January 1, 1970");
});
SpecEnd
對(duì)于 MVVM
來說耻蛇,我們可以把 view
看作是 viewModel
的可視化形式踪蹬,viewModel
提供了 view
所需的數(shù)據(jù)和命令胞此。因此,viewModel
的可測(cè)試性可以幫助我們極大地提高應(yīng)用的質(zhì)量跃捣。
MVVMReactiveCocoa
接下來漱牵,我們進(jìn)入本文的第二部分,重點(diǎn)介紹一個(gè)使用 MVVM
和 RAC
開發(fā)的開源項(xiàng)目 MVVMReactiveCocoa
疚漆。說明布疙,本文將主要介紹這個(gè)應(yīng)用的架構(gòu)和設(shè)計(jì)思路,希望可以為你實(shí)踐 MVVM
提供一個(gè)真實(shí)的參考案例愿卸,有些架構(gòu)并非是 MVVM
所必須的灵临,而是我們?yōu)榱烁槙车厥褂?MVVM
而引入的,特別是 ViewModel-Based Navigation
趴荸。所以儒溉,請(qǐng)你在實(shí)踐的過程中能夠結(jié)合自身應(yīng)用的實(shí)際情況做出相應(yīng)的取舍,靈活處理发钝。最后顿涣,我們將以登錄界面為例,一起探討下 MVVM
的實(shí)踐思路酝豪。
說明横侦,以下內(nèi)容均基于 MVVMReactiveCocoa
的 v2.1.1 標(biāo)簽進(jìn)行展開,并且對(duì)部分無關(guān)代碼做了刪減基跑。
類圖
為了方便我們從宏觀上了解 MVVMReactiveCocoa
的整體結(jié)構(gòu)闷沥,我們先來看看它的類圖:
從上圖中,我們可以看到瘫证,在 MVVMReactiveCocoa
中主要有兩大繼承體系:
- 用藍(lán)色標(biāo)識(shí)出來的
viewModel
的繼承體系揉阎,基類為MRCViewModel
; - 用紅色標(biāo)識(shí)出來的
view
的繼承體系背捌,基類為MRCViewController
毙籽。
除了提供與系統(tǒng)基類 UIViewController
相對(duì)應(yīng)的基類 MRCViewModel/MRCViewController
外,還提供了與系統(tǒng)基類 UITableViewController
和 UITabBarController
相對(duì)應(yīng)的基類 MRCTableViewModel/MRCTableViewController
和 MRCTabBarViewModel/MRCTabBarController
毡庆,其中基類 MRCTableViewModel/MRCTableViewController
的使用最為普遍坑赡。
說明,之所以通過基類的方式來組織 MVVMReactiveCocoa
么抗,一方面是因?yàn)橹饕_發(fā)者只有我一個(gè)人毅否,這個(gè)方案非常容易實(shí)施;另一方面是因?yàn)橥ㄟ^基類的方式可以盡可能簡(jiǎn)單地實(shí)現(xiàn)代碼重用乖坠,提高開發(fā)效率搀突。
服務(wù)總線
經(jīng)過前面的探討,我們已經(jīng)知道了 MVVM
中的 viewModel
的主要職責(zé)就是從 model
層獲取 view
所需的數(shù)據(jù),并且將這些數(shù)據(jù)轉(zhuǎn)換成 view
能夠展示的形式仰迁。因此甸昏,為了方便 viewModel
層調(diào)用 model
層中的所有服務(wù),并且統(tǒng)一管理這些服務(wù)的創(chuàng)建徐许,我使用抽象工廠模式將 model
層的所有服務(wù)集中管理了起來施蜜,結(jié)構(gòu)圖如下:
從上圖中,我們可以看出雌隅,在服務(wù)總線類 MRCViewModelServices/MRCViewModelServicesImpl
中翻默,主要包括以下三個(gè)方面的內(nèi)容:
- 應(yīng)用自有的服務(wù)類,用柚黃色進(jìn)行了標(biāo)識(shí)恰起,包括
MRCAppStoreService/MRCAppStoreServiceImpl
和MRCRepositoryService/MRCRepositoryServiceImpl
兩個(gè)服務(wù)類修械; - 第三方
GitHub
提供的API
框架,用天藍(lán)色進(jìn)行了標(biāo)識(shí)检盼,主要包括OCTClient
服務(wù)類肯污; - 應(yīng)用的導(dǎo)航服務(wù),用藻綠色進(jìn)行了標(biāo)識(shí)吨枉,包括
MRCNavigationProtocol
協(xié)議和實(shí)現(xiàn)類MRCViewModelServicesImpl
等蹦渣。
其中,前兩者都是以信號(hào)的形式對(duì) viewModel
層提供服務(wù)貌亭,代表異步的網(wǎng)絡(luò)請(qǐng)求等數(shù)據(jù)獲取操作柬唯,而我們?cè)?viewModel
層則可以通過訂閱信號(hào)的形式獲取到所需的數(shù)據(jù)。此外圃庭,服務(wù)總線還實(shí)現(xiàn)了 MRCNavigationProtocol
協(xié)議锄奢,它的內(nèi)容如下:
@protocol MRCNavigationProtocol <NSObject>
- (void)pushViewModel:(MRCViewModel *)viewModel animated:(BOOL)animated;
- (void)popViewModelAnimated:(BOOL)animated;
- (void)popToRootViewModelAnimated:(BOOL)animated;
- (void)presentViewModel:(MRCViewModel *)viewModel animated:(BOOL)animated completion:(VoidBlock)completion;
- (void)dismissViewModelAnimated:(BOOL)animated completion:(VoidBlock)completion;
- (void)resetRootViewModel:(MRCViewModel *)viewModel;
@end
看上去是不是有點(diǎn)眼熟?是的冤议,MRCNavigationProtocol
協(xié)議其實(shí)就是參照系統(tǒng)的導(dǎo)航操作定義出來的斟薇,用來實(shí)現(xiàn) ViewModel-Based
的導(dǎo)航服務(wù)。注意恕酸,服務(wù)總線類 MRCViewModelServicesImpl
其實(shí)并沒有真正實(shí)現(xiàn) MRCNavigationProtocol
協(xié)議中聲明的操作,只不過是實(shí)現(xiàn)了一些空操作而已:
- (void)pushViewModel:(MRCViewModel *)viewModel animated:(BOOL)animated {}
- (void)popViewModelAnimated:(BOOL)animated {}
- (void)popToRootViewModelAnimated:(BOOL)animated {}
- (void)presentViewModel:(MRCViewModel *)viewModel animated:(BOOL)animated completion:(VoidBlock)completion {}
- (void)dismissViewModelAnimated:(BOOL)animated completion:(VoidBlock)completion {}
- (void)resetRootViewModel:(MRCViewModel *)viewModel {}
那么胯陋,我們是怎么實(shí)現(xiàn) ViewModel-Based
的導(dǎo)航操作的呢蕊温?用 MRCViewModelServicesImpl
來實(shí)現(xiàn)這些空操作到底有什么用意?為什么要這么做遏乔,目的是為了什么义矛?兄臺(tái),莫急盟萨,請(qǐng)接著看下一小節(jié)的內(nèi)容凉翻。
ViewModel-Based Navigation
我們先來思考一個(gè)問題,就是我們?yōu)槭裁匆獙?shí)現(xiàn) ViewModel-Based
的導(dǎo)航操作呢捻激?直接在 view
層使用系統(tǒng)的 push/present
等操作來完成導(dǎo)航不就好了么制轰?我總結(jié)了一下這么做的理由前计,主要有以下三點(diǎn):
- 從理論上來說,
MVVM
模式的應(yīng)用應(yīng)該是以viewModel
為驅(qū)動(dòng)來運(yùn)轉(zhuǎn)的垃杖; - 根據(jù)我們前面對(duì)
MVVM
的探討男杈,viewModel
提供了view
所需的數(shù)據(jù)和命令。因此调俘,我們往往可以直接在命令執(zhí)行成功后使用doNext
順帶就把導(dǎo)航操作給做了伶棒,一氣呵成; - 這樣可以使
view
更加輕量級(jí)彩库,只需要綁定viewModel
提供的數(shù)據(jù)和命令即可肤无。
既然如此,那我們究竟要如何實(shí)現(xiàn) ViewModel-Based
的導(dǎo)航操作呢骇钦?我們都知道 iOS
中的導(dǎo)航操作無外乎兩種宛渐,push/pop
和 present/dismiss
,前者是 UINavigationController
特有的功能司忱,而后者是所有 UIViewController
都具備的功能皇忿。注意,UINavigationController
也是 UIViewController
的子類坦仍,所以它也同樣具備 present/dismiss
的功能鳍烁。因此,從本質(zhì)上來說繁扎,不管我們要實(shí)現(xiàn)什么樣的導(dǎo)航操作幔荒,最終都是離不開 push/pop
和 present/dismiss
的。
目前梳玫,MVVMReactiveCocoa
的做法是在 view
層維護(hù)一個(gè) NavigationController
的堆棧 MRCNavigationControllerStack
爹梁,不管是 push/pop
還是 present/dismiss
,都使用棧頂?shù)?NavigationController
來執(zhí)行導(dǎo)航操作提澎,并且保證 present
出來的是一個(gè) NavigationController
姚垃。
接下來,我們一起來看看 MVVMReactiveCocoa
在執(zhí)行了 push/pop
或 present/dismiss
操作后視圖層次結(jié)構(gòu)的變化過程盼忌。首先积糯,我們來看看用戶在登錄成功后進(jìn)入到首頁(yè)時(shí)應(yīng)用的視圖層次結(jié)構(gòu)圖:
此時(shí),應(yīng)用展示的界面是 NewsViewController
谦纱。在 MRCNavigationControllerStack
堆棧中只有 NavigationController0
一個(gè)元素看成;而 NavigationController1
并沒有在 MRCNavigationControllerStack
堆棧中,這是因?yàn)樾枰С?TabBarController
的滑動(dòng)切換而設(shè)計(jì)的視圖層次結(jié)構(gòu)跨嘉,是首頁(yè)比較特殊的一個(gè)地方川慌。更多信息可以查看 GitHub
開源庫(kù) WXTabBarController ,在這里,我們不用太過于關(guān)心這個(gè)問題梦重,只需要理解原理就好了兑燥。
接下來,當(dāng)用戶在 NewsViewController
界面忍饰,點(diǎn)擊了某一個(gè) cell
贪嫂,通過 push
的方式,進(jìn)入到倉(cāng)庫(kù)詳情界面時(shí)艾蓝,應(yīng)用的視圖層次結(jié)構(gòu)圖如下:
應(yīng)用通過 MRCNavigationControllerStack
棧頂?shù)脑?NavigationController0
力崇,將倉(cāng)庫(kù)詳情界面 push
到了自身的堆棧中。此時(shí)赢织,應(yīng)用展示的界面是被 push
進(jìn)來的倉(cāng)庫(kù)詳情界面 RepoDetailViewController
亮靴。最后,當(dāng)用戶在倉(cāng)庫(kù)詳情界面于置,點(diǎn)擊左下角的切換分支按鈕茧吊,通過 present
的方式,彈出分支選擇界面時(shí)八毯,應(yīng)用的視圖層次結(jié)構(gòu)圖如下:
應(yīng)用通過 MRCNavigationControllerStack
棧頂?shù)脑?NavigationController0
搓侄,將 NavigationController5
以 present
的方式彈出來。此時(shí)话速,應(yīng)用展示的是 NavigationController5
的根視圖 SelectBranchOrTagViewController
讶踪。說明,由于 pop
和 dismiss
與 push
和 present
互為逆操作泊交,所以只要按照從下到上的順序看上面的視圖層次結(jié)構(gòu)圖即可乳讥,這里不再贅述。
等等廓俭,如果我沒有記錯(cuò)的話云石,MRCNavigationControllerStack
堆棧是在 view
層,而服務(wù)總線類 MRCViewModelServicesImpl
是在 viewModel
層的研乒。據(jù)我所知汹忠,viewModel
層是不能引入 view
層的任何東西的,更嚴(yán)格的說雹熬,是不能引入任何 UIKit
中的東西的错维,否則就違背了 MVVM
的基本原則,并且也會(huì)散失 viewModel
的可測(cè)試性橄唬。在這個(gè)前提下,你要如何讓這兩者產(chǎn)生關(guān)聯(lián)呢参歹?
沒錯(cuò)仰楚,這就是 MRCViewModelServicesImpl
中之所以實(shí)現(xiàn)那些空操作的目的所在了。viewModel
通過調(diào)用 MRCViewModelServicesImpl
中的空操作來表明需要執(zhí)行相應(yīng)的導(dǎo)航操作,而 MRCNavigationControllerStack
則通過 Hook
來捕獲這些空操作僧界,然后使用棧頂?shù)?NavigationController
來執(zhí)行真正的導(dǎo)航操作:
- (void)registerNavigationHooks {
@weakify(self)
[[(NSObject *)self.services
rac_signalForSelector:@selector(pushViewModel:animated:)]
subscribeNext:^(RACTuple *tuple) {
@strongify(self)
UIViewController *viewController = (UIViewController *)[MRCRouter.sharedInstance viewControllerForViewModel:tuple.first];
[self.navigationControllers.lastObject pushViewController:viewController animated:[tuple.second boolValue]];
}];
[[(NSObject *)self.services
rac_signalForSelector:@selector(popViewModelAnimated:)]
subscribeNext:^(RACTuple *tuple) {
@strongify(self)
[self.navigationControllers.lastObject popViewControllerAnimated:[tuple.first boolValue]];
}];
[[(NSObject *)self.services
rac_signalForSelector:@selector(popToRootViewModelAnimated:)]
subscribeNext:^(RACTuple *tuple) {
@strongify(self)
[self.navigationControllers.lastObject popToRootViewControllerAnimated:[tuple.first boolValue]];
}];
[[(NSObject *)self.services
rac_signalForSelector:@selector(presentViewModel:animated:completion:)]
subscribeNext:^(RACTuple *tuple) {
@strongify(self)
UIViewController *viewController = (UIViewController *)[MRCRouter.sharedInstance viewControllerForViewModel:tuple.first];
UINavigationController *presentingViewController = self.navigationControllers.lastObject;
if (![viewController isKindOfClass:UINavigationController.class]) {
viewController = [[MRCNavigationController alloc] initWithRootViewController:viewController];
}
[self pushNavigationController:(UINavigationController *)viewController];
[presentingViewController presentViewController:viewController animated:[tuple.second boolValue] completion:tuple.third];
}];
[[(NSObject *)self.services
rac_signalForSelector:@selector(dismissViewModelAnimated:completion:)]
subscribeNext:^(RACTuple *tuple) {
@strongify(self)
[self popNavigationController];
[self.navigationControllers.lastObject dismissViewControllerAnimated:[tuple.first boolValue] completion:tuple.second];
}];
[[(NSObject *)self.services
rac_signalForSelector:@selector(resetRootViewModel:)]
subscribeNext:^(RACTuple *tuple) {
@strongify(self)
[self.navigationControllers removeAllObjects];
UIViewController *viewController = (UIViewController *)[MRCRouter.sharedInstance viewControllerForViewModel:tuple.first];
if (![viewController isKindOfClass:[UINavigationController class]]) {
viewController = [[MRCNavigationController alloc] initWithRootViewController:viewController];
((UINavigationController *)viewController).delegate = self;
[self pushNavigationController:(UINavigationController *)viewController];
}
MRCSharedAppDelegate.window.rootViewController = viewController;
}];
}
通過 Hook
的方式侨嘀,我們最終實(shí)現(xiàn)了 ViewModel-Based
的導(dǎo)航操作,并且在 viewModel
層中也沒有引入 view
層的任意東西捂襟,實(shí)現(xiàn)了解耦合咬腕。
Router
還有一點(diǎn)值得一提的是,我們?cè)?viewModel
中調(diào)用導(dǎo)航操作的時(shí)候葬荷,只傳入了 viewModel
的實(shí)例作為參數(shù)涨共,那么我們?cè)?MRCNavigationControllerStack
中執(zhí)行真正的導(dǎo)航操作時(shí),怎么才能知道要跳轉(zhuǎn)到哪個(gè)界面呢宠漩?為此举反,我們配置了一個(gè)從 viewModel
到 view
的映射,并且約定了一個(gè)統(tǒng)一的初始化 view
的方法 initWithViewModel:
:
- (MRCViewController *)viewControllerForViewModel:(MRCViewModel *)viewModel {
NSString *viewController = self.viewModelViewMappings[NSStringFromClass(viewModel.class)];
NSParameterAssert([NSClassFromString(viewController) isSubclassOfClass:[MRCViewController class]]);
NSParameterAssert([NSClassFromString(viewController) instancesRespondToSelector:@selector(initWithViewModel:)]);
return [[NSClassFromString(viewController) alloc] initWithViewModel:viewModel];
}
- (NSDictionary *)viewModelViewMappings {
return @{
@"MRCLoginViewModel": @"MRCLoginViewController",
@"MRCHomepageViewModel": @"MRCHomepageViewController",
@"MRCRepoDetailViewModel": @"MRCRepoDetailViewController",
...
};
}
登錄界面
最后扒吁,我們一起來看看登錄界面中 viewModel
和 view
的部分關(guān)鍵代碼火鼻,探討一下 MVVM
的具體實(shí)踐過程。說明雕崩,我們將會(huì)盡可能地回避具體的業(yè)務(wù)邏輯魁索,重點(diǎn)關(guān)注 MVVM
的實(shí)踐思路。下面是登錄界面的截圖:
其中盼铁,主要的界面元素有:
- 一個(gè)用于展示用戶頭像的按鈕
avatarButton
粗蔚; - 用于輸入賬號(hào)和密碼的輸入框
usernameTextField
和passwordTextField
; - 一個(gè)直接登錄的按鈕
loginButton
和一個(gè)跳轉(zhuǎn)到瀏覽器授權(quán)登錄的按鈕browserLoginButton
捉貌。
分析:根據(jù)我們前面對(duì) MVVM
的探討支鸡,viewModel
需要提供 view
所需的數(shù)據(jù)和命令。因此趁窃,MRCLoginViewModel.h
頭文件的內(nèi)容大致如下:
@interface MRCLoginViewModel : MRCViewModel
@property (nonatomic, copy, readonly) NSURL *avatarURL;
@property (nonatomic, copy) NSString *username;
@property (nonatomic, copy) NSString *password;
@property (nonatomic, strong, readonly) RACSignal *validLoginSignal;
@property (nonatomic, strong, readonly) RACCommand *loginCommand;
@property (nonatomic, strong, readonly) RACCommand *browserLoginCommand;
@end
非常直觀牧挣,其中需要特別說明的是 validLoginSignal
屬性代表的是登錄按鈕是否可用,它將會(huì)與 view
中登錄按鈕的 enabled
屬性進(jìn)行綁定醒陆。接著瀑构,我們來看看 MRCLoginViewModel.m
的實(shí)現(xiàn)文件中的部分關(guān)鍵代碼:
@implementation MRCLoginViewModel
- (void)initialize {
[super initialize];
RAC(self, avatarURL) = [[RACObserve(self, username)
map:^(NSString *username) {
return [[OCTUser mrc_fetchUserWithRawLogin:username] avatarURL];
}]
distinctUntilChanged];
self.validLoginSignal = [[RACSignal
combineLatest:@[ RACObserve(self, username), RACObserve(self, password) ]
reduce:^(NSString *username, NSString *password) {
return @(username.length > 0 && password.length > 0);
}]
distinctUntilChanged];
@weakify(self)
void (^doNext)(OCTClient *) = ^(OCTClient *authenticatedClient) {
@strongify(self)
MRCHomepageViewModel *viewModel = [[MRCHomepageViewModel alloc] initWithServices:self.services params:nil];
dispatch_async(dispatch_get_main_queue(), ^{
[self.services resetRootViewModel:viewModel];
});
};
[OCTClient setClientID:MRC_CLIENT_ID clientSecret:MRC_CLIENT_SECRET];
self.loginCommand = [[RACCommand alloc] initWithSignalBlock:^(NSString *oneTimePassword) {
@strongify(self)
OCTUser *user = [OCTUser userWithRawLogin:self.username server:OCTServer.dotComServer];
return [[OCTClient
signInAsUser:user password:self.password oneTimePassword:oneTimePassword scopes:OCTClientAuthorizationScopesUser | OCTClientAuthorizationScopesRepository note:nil noteURL:nil fingerprint:nil]
doNext:doNext];
}];
self.browserLoginCommand = [[RACCommand alloc] initWithSignalBlock:^(id input) {
return [[OCTClient
signInToServerUsingWebBrowser:OCTServer.dotComServer scopes:OCTClientAuthorizationScopesUser | OCTClientAuthorizationScopesRepository]
doNext:doNext];
}];
}
@end
- 當(dāng)用戶輸入的用戶名發(fā)生變化時(shí),調(diào)用
model
層的方法查詢本地?cái)?shù)據(jù)庫(kù)中緩存的用戶數(shù)據(jù)刨摩,并返回avatarURL
屬性; - 當(dāng)用戶輸入的用戶名或密碼發(fā)生變化時(shí)寺晌,判斷用戶名和密碼的長(zhǎng)度是否均大于
0
,如果是則登錄按鈕可用澡刹,否則不可用; - 當(dāng)
loginCommand
或browserLoginCommand
命令執(zhí)行成功時(shí)呻征,調(diào)用doNext
代碼塊,使用服務(wù)總線中的方法resetRootViewModel:
進(jìn)入首頁(yè)罢浇。
接下來陆赋,我們來看看 MRCLoginViewController
中的部分關(guān)鍵代碼:
@implementation MRCLoginViewController
- (void)bindViewModel {
[super bindViewModel];
@weakify(self)
[RACObserve(self.viewModel, avatarURL) subscribeNext:^(NSURL *avatarURL) {
@strongify(self)
[self.avatarButton sd_setImageWithURL:avatarURL forState:UIControlStateNormal placeholderImage:[UIImage imageNamed:@"default-avatar"]];
}];
RAC(self.viewModel, username) = self.usernameTextField.rac_textSignal;
RAC(self.viewModel, password) = self.passwordTextField.rac_textSignal;
RAC(self.loginButton, enabled) = self.viewModel.validLoginSignal;
[[self.loginButton
rac_signalForControlEvents:UIControlEventTouchUpInside]
subscribeNext:^(id x) {
@strongify(self)
[self.viewModel.loginCommand execute:nil];
}];
[[self.browserLoginButton
rac_signalForControlEvents:UIControlEventTouchUpInside]
subscribeNext:^(id x) {
@strongify(self)
NSString *message = [NSString stringWithFormat:@"“%@” wants to open “Safari”", MRC_APP_NAME];
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:nil
message:message
preferredStyle:UIAlertControllerStyleAlert];
[alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:NULL]];
[alertController addAction:[UIAlertAction actionWithTitle:@"Open" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
@strongify(self)
[self.viewModel.browserLoginCommand execute:nil];
}]];
[self presentViewController:alertController animated:YES completion:NULL];
}];
}
@end
- 觀察
viewModel
中avatarURL
屬性的變化沐祷,然后設(shè)置avatarButton
中的圖片; - 將
viewModel
中的username
和password
屬性分別與usernameTextField
和passwordTextField
輸入框中的內(nèi)容進(jìn)行綁定攒岛; - 將
loginButton
的enabled
屬性與viewModel
的validLoginSignal
屬性進(jìn)行綁定赖临; - 在
loginButton
和browserLoginButton
按鈕被點(diǎn)擊時(shí)分別執(zhí)行loginCommand
和browserLoginCommand
命令。
綜上所述灾锯,我們將 MRCLoginViewController
中的展示邏輯抽取到 MRCLoginViewModel
中后兢榨,使得 MRCLoginViewController
中的代碼更加簡(jiǎn)潔和清晰。實(shí)踐 MVVM
的關(guān)鍵點(diǎn)在于顺饮,我們要能夠分析清楚 viewModel
需要暴露給 view
的數(shù)據(jù)和命令吵聪,這些數(shù)據(jù)和命令能夠代表 view
當(dāng)前的狀態(tài)。
總結(jié)
首先领突,我們從理論出發(fā)介紹了 MVC
和 MVVM
各自的概念以及從 MVC
到 MVVM
的演進(jìn)過程暖璧;接著,介紹了 RAC
在 MVVM
中的兩個(gè)使用場(chǎng)景君旦;最后澎办,我們從實(shí)踐的角度,重點(diǎn)介紹了一個(gè)使用 MVVM
和 RAC
開發(fā)的開源項(xiàng)目 MVVMReactiveCocoa
金砍【质矗總的來說,我認(rèn)為 iOS
中的 MVVM
可以分為以下三種不同的實(shí)踐程度恕稠,它們分別對(duì)應(yīng)不同的適用場(chǎng)景:
-
MVVM + KVO
琅绅,適用于現(xiàn)有的MVC
項(xiàng)目,想轉(zhuǎn)換成MVVM
但是不打算引入RAC
作為binder
的團(tuán)隊(duì)鹅巍; -
MVVM + RAC
千扶,適用于現(xiàn)有的MVC
項(xiàng)目,想轉(zhuǎn)換成MVVM
并且打算引入RAC
作為binder
的團(tuán)隊(duì)骆捧; -
MVVM + RAC + ViewModel-Based Navigation
澎羞,適用于全新的項(xiàng)目,想實(shí)踐MVVM
并且打算引入RAC
作為binder
敛苇,然后也想實(shí)踐ViewModel-Based Navigation
的團(tuán)隊(duì)妆绞。
寫在最后,希望這篇文章能夠打消你對(duì) MVVM
模式的顧慮枫攀,趕快行動(dòng)起來吧括饶。
原文鏈接:http://blog.leichunfeng.com/blog/2016/02/27/mvvm-with-reactivecocoa/