閑話: 聽說學(xué) Haskell 可以打開新世界的大門
剛剛發(fā)現(xiàn) ReactiveCocoa 的時候,看到相關(guān)的術(shù)語 signal、subscriber 之類的,不明覺厲。再加上 FRP - Functional Reactive Programming,我似乎看到了新世界的大門。
作為初見,希望盡可能地提取關(guān)鍵概念來理解這個非常熱門但是有些難懂的框架,減少打開新世界大門的阻力。
<br /><br />
編程范式 Programming Paradigm
在 Wikipedia 上搜索這個關(guān)鍵詞的話,就可以看到在這個詞條右邊列出了幾十個編程范式,領(lǐng)略一下前人的腦洞。<br />
( ̄ε(# ̄)☆╰╮o( ̄皿 ̄///)
咳咳,對于 ReactiveCocoa 這個框架最先應(yīng)該了解的是 Functional Programming 和 Reactive Programming
函數(shù)式編程 Functional Programming
參考 Wikipedia 我的理解是:
- 函數(shù)可以作為參數(shù)傳遞
- 組合各種函數(shù)來實現(xiàn)所需
還有純函數(shù)式編程語言里沒有變量之類的暫時不去深究。這個范式其實在現(xiàn)代編程語言中大多都支持。
響應(yīng)式編程 Reactive Programming
重點就一個,數(shù)據(jù)可以隨著事件動態(tài)變化,就如同 Wiki 中所說的,表達(dá)式的結(jié)果會因為表達(dá)式中的變量改變自動更新。
那么上面兩種范式結(jié)合之后是什么?
函數(shù)響應(yīng)式編程 Functional Reactive Programming
重點還是在響應(yīng),通過組合函數(shù)可以實現(xiàn)復(fù)雜的響應(yīng)過程。
希望詳細(xì)了解,這里 有一篇很好的關(guān)于 FRP 的文章
<br />
MVC vs MVVM
iOS 開發(fā)過程中會遇到在一個 View Controller 的文件里,有著幾百上千行的代碼。View Controller 總是承擔(dān)著過多的任務(wù),這里 MVVM 的出現(xiàn)就是為了剝離 View Controller 中過多的代碼,objc.io 的第一個 issue 就是一篇分離 View Controller 和 Table View 的很好的文章,英文版,中文版。
另外 這里 有一篇很好的用 ReactiveCocoa 實現(xiàn) MVVM 的文章,重點是輕量化 ViewController,組合兩種架構(gòu),不是完全替換 MVC。
<br />
Reactive Cocoa
終于進入正題。這個框架,就是吸收了如同 Haskell 這類函數(shù)式語言的思想,從微軟的 Rx 演化來的。利用它就可以很好的實現(xiàn) MVVM 的架構(gòu),防止臃腫雜亂的 View Controller。
使用它會進入完全不同的另一種編程思維,用這另一種思維去看以前遇到的問題,就看到新世界的大門了<br />
(??????)??
RAYWENDERLICH 上的 一篇文章 很詳細(xì)的介紹了框架基本的用法。不過無論是這篇還是上一節(jié)的那篇文章,篇幅都很長,根據(jù)本文的初衷,下面總結(jié)一下。
信號 Signal
這個就是最核心的概念,操作都基于對信號的各種處理上。信號就是用來 承載 數(shù)據(jù)的,跟滿天飛來飛去的無線電波一樣。我們可以對信號做各種處理,像是監(jiān)聽、過濾等等,在 Reactive Cocoa 中的信號非常類似于人們自然理解的信號。
操作符 Operator
處理信號使用操作符,代碼上其實就是一個參數(shù)是 block 的函數(shù)。block 里面就是怎么處理信號。框架里面提供了很多操作符,參看 github 上的 文檔。
<br />
應(yīng)用內(nèi)切換語言
如果已經(jīng)看過之前提到的兩篇教程文章,相比那接下來的示例會更簡單。之前兩篇文章都是用了搜索 Twitter 的推文來展示框架的用法,鑒于你懂得的原因,和新浪微博的接口復(fù)雜一些,搜索功能也很限制,所以我用了這個想到就會覺得實現(xiàn)起來會很麻煩的功能。
產(chǎn)品的國際化就像牙線:所有人都知道他們應(yīng)該使用,卻可能都不去用。 -- NSHipster
這個例子的完整代碼可以在 github 上找到 RAC-International-Example
首先我們先看一下成果

最終在我們需要國際化的地方的代碼長這樣
@weakify(self);
[LanguageChangedSignal subscribeNext:^(NSString *languageCode) {
@strongify(self);
self.languageButton.title = LocalizedString(@"Language");
self.titleLabel.text = LocalizedString(@"Hello World");
[self.button setTitle:LocalizedString(@"Button") forState:UIControlStateNormal];
self.label.text = LocalizedString(@"Label");
self.textView.text = LocalizedString(@"It's a pretty day.");
}];
在任何需要國際化的地方只要這么寫就可以,其實國際化就是一勞永逸的工作,習(xí)慣之后其實非常簡單。
@weakify(self) 和 @strongify(self) 是用來方便地解決循環(huán)引用的,需要另外包含頭文件 #import "EXTScope.h"。
subscribeNext: 方法是訂閱信號,會在信號發(fā)送 sendNext 時執(zhí)行 block 內(nèi)的代碼,這里就是刷新 UI
下面是如何發(fā)送信號
創(chuàng)建語言管理(視圖模型)類
我們整個的信號流程很簡單:修改語言(變化產(chǎn)生數(shù)據(jù)流) --> 加載語言文件 --> 刷新 UI
首先呢,不得不放棄 NSLocalizedString 的方法,我還沒找到可以直接修改地區(qū)的方法。
創(chuàng)建一個語言管理的單例類
@class RACSignal;
@interface LanguageManager : NSObject
+ (LanguageManager *)shareInstance;
- (RACSignal *)languageChangedSignal;
- (NSString *)localizedString:(NSString *)key;
- (NSArray *)languages;
- (void)changeLanguageTo:(NSString *)language;
@end
#define LanguageViewModel [LanguageManager shareInstance]
#define LocalizedString(key) [LanguageViewModel localizedString:(key)]
#define LanguageChangedSignal [LanguageViewModel languageChangedSignal]
languageChangedSignal 方法返回了一個語言變化的信號,用來給全局需要變化的地方訂閱用<br />
localizedString: 方法獲取當(dāng)前語言的字符串<br />
languages 方法返回所有支持的語言列表,在語言選擇的 Table View 里使用<br />
changeLanguageTo: 方法用來變更當(dāng)前語言<br />
另外下面添加了幾個使代碼整潔的宏。
我們只需要關(guān)注 languageChangedSignal 的實現(xiàn)
- (RACSignal *)languageChangedSignal {
if (!_languageChangedSignal) {
@weakify(self);
self.languageChangedSignal = [RACObserve(self, currentLanguage) doNext:^(NSString *currentLanguage) {
@strongify(self);
[[NSUserDefaults standardUserDefaults] setObject:currentLanguage forKey:@"currentLanguage"];
NSBundle *localizeBundle = [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:self.currentLanguage ofType:@"lproj"]];
self.stringsFile = [[NSDictionary alloc] initWithContentsOfFile:[localizeBundle pathForResource:LocalizationFile ofType:@"strings"]];
if (!self.stringsFile) {
NSBundle *baseBunble = [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:@"Base" ofType:@"lproj"]];
self.stringsFile = [[NSDictionary alloc] initWithContentsOfFile:[baseBunble pathForResource:LocalizationFile ofType:@"strings"]];
}
}];
}
return _languageChangedSignal;
}
這段代碼看起來比較亂,只需要關(guān)注 RACObserve 這個框架提供的宏和 doNext: 方法。
RACObserve(self, currentLanguage) 就是創(chuàng)建了 currentLanguage 這個屬性的變化的信號。<br />
doNext:^(NSString *currentLanguage) {...} 這個方法是在更改 UI 之前插入需要執(zhí)行的動作。block 中略長的內(nèi)容是根據(jù)語言代碼獲取對應(yīng)的 strings 文件。
Reactive Cocoa 是基于 KVO 的,所以要注意觀察的屬性是不是支持 KVO。在這里就是如果你使用下劃線的熟悉去修改,就不會發(fā)生任何你想要的事,需要使用 setter 的方式去修改(self.currentLanguage)。
接下來
其實沒有接下來了,沒錯,就是這么簡單,核心部分的代碼就是這樣。
其他細(xì)節(jié)實現(xiàn)可以 clone 或下載本示例項目 RAC-International-Example
<br />
新世界的大門
新世界的大門打開了,使用 Reactive Cocoa 確實讓代碼變得很不同,雖然很多陌生的概念,但是當(dāng)你熟悉和深入了解之后,他就是進入新世界的鑰匙。
<br />
附錄
- Github Repo - ReactiveCocoa
- Wikipedia - Functional Programming
- Wikipedia - Reactive Programming
- 函數(shù)式反應(yīng)型編程(FRP) —— 實時互動應(yīng)用開發(fā)的新思路
- objc.io - Lighter View Controllers
- Sprynthesis - ReactiveCocoa and MVVM, an Introduction
- RayWenderlich ReactiveCocoa Tutorial – The Definitive Introduction
- Github - ReactiveCocoa Documentation
- NSHipster - NSLocale
- 示例代碼 - RAC-International-Example