淺析 iOS 開發頁面之間資料傳遞的方式
iOS 開發中 VC 之間資料的傳遞和交換可以有很多種方法,下面介紹常見的幾種方式,並且從設計和架構的層次上分析一下不同方法的優缺點和適用場景。內容主要來自於筆者自己在開發中的實踐和思考,如果有錯誤或者遺漏,歡迎聯絡指正。
為了方便描述,我們舉一個實際的用例。假設我們有一個設定介面 SettingViewController,其中有一項是語言設定,顯示當前選擇的語言。
點進去之後進入到具體的語言設定介面 LanguageViewController,其中是漢語,英語,法語等一系列的選項。
現在假設我們在 SettingVC 中知道使用者的選項是"英語"。為了讓使用者的體驗更好,在使用者點選"語言設定"進入 LanguageVC 的時候,裡面"英語"這一項應該就處於選擇狀態了,這時候就需要進行 SettingVC -> LanguageVC 的正向資料傳遞。同樣,當用戶在 LanguageVC 中選擇了一個新的語言,SettingVC 需要根據使用者的選擇更新自己的顯示,這裡就需要 LanguageVC -> SettingVC 的反向資料傳遞。
正向傳遞
使用 property
首先我們來看正向的資料傳遞。正向傳遞最容易想到的就是使用 @property 了,我們可以在 LanguageVC 中定義一個屬性:
@property (nonatomic) NSUInteger selectedIndex;
然後在向 LanguageVC push 的時候把它設定好:
LanguageViewController *vc = [[LanguageViewController alloc] init]; vc.selectedIndex = self.currentSelectedIndex; [self.navigationController pushViewController:vc animated:YES];
使用 initializer
使用屬性很簡單,但是當要設定的屬性很多時,會導致程式碼過於冗餘。如果要設定很多屬性,我們可以通過自定義 init
方法來簡化程式碼:
LanguageViewController *vc = [[LanguageViewController alloc] initWithIndex:self.currentSelectedIndex name:self.languageName];
使用 Router
上面的實現雖然達到了目的,但是卻導致了 SettingVC 和 LanguageVC 之間出現了強耦合。為了避免 VC 之間的強耦合,借鑑 Web 開發的思想,我們可以引入一箇中介性質的 router,來完成 VC 之間的資料傳遞。以
[[HHRouter shared] map:@"/lang/:index/" toControllerClass:[LanguageViewController class]];
在 LanguageVC 中實現 setParams:
用來接收引數:
- (void)setParams:(NSDictionary)params {
self.selectedIndex = [params[@"index"] integerValue];
}
然後通過帶引數的 URL,來取出對應的 VC:
UIViewController *vc = [[HHRouter shared] matchController:@"/lang/1/"];
[self.navigationController pushViewController:vc animated:YES];
這樣便實現了引數的傳遞,同時並沒有引入對於 LanguageVC 的強依賴,是一種比較好的解決方案。
在 MVVMReactiveCocoa 中,作者使用了一種基於 Model 的 router,同樣實現了類似的效果,有興趣的可以去看一下它的原始碼。
反向傳遞
反向傳遞相對於正向傳遞要難一些,最容易想到的方法就是在 LanguageVC 中保留一個 SettingVC 的弱引用,然後呼叫有關的方法,但是這種做法又帶來了強依賴,因此不建議使用。通常使用的方法有下面幾種:
Delegate
Delegate(委託) 是 iOS 系統庫中大量使用的一種設計模式。委託可以很方便地做到介面和實現的分離,降低了依賴性(依賴於介面而不依賴於實現)。我們也可以使用委託模式來實現反向的資料傳遞。
首先我們定義一個 Protocol,同時在 LanguageVC 中設定一個實現了這個 Protocol 的 delegate 物件屬性:
@protocol LanguageSelectionProtocol <NSObject>
@required
- (void)didSelectLanguageAtIndex:(NSUInteger)index;
@end
@interface LanguageViewController : UITableViewController
@property (nonatomic, weak) id<LanguageSelectionProtocol> delegate;
@end
當用戶在 LanguageVC 中做出選擇時,我們可以呼叫 delegate 方法來實現資料回撥:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if ([self.delegate respondsToSelector:@selector(didSelectLanguageAtIndex:)]) {
[self.delegate didSelectLanguageAtIndex:indexPath.row];
}
}
注意到由於我們使用了 delegate,我們並沒有強依賴於 SettingVC,不管是什麼物件,只要是實現了對應 Protocol 的都是可以的。
在 SettingVC 中,只要把自己設定為 LanguageVC 的 delegate ,然後實現對應的 protocol 方法就可以了:
LanguageViewController *vc = [[LanguageViewController alloc] init];
vc.delegate = self;
[self.navigationController pushViewController:vc animated:YES];
優點: Delegate 實現了完全的面向介面程式設計,達到了很好的解耦效果。
缺點: 使用略顯繁瑣,需要定義對應的 Protocol 和持有 delegate 物件。另外,當一個 VC 實現了過多的 Protocol 的時候,會導致程式碼難以維護。
Block
從 iOS 4.0 開始我們可以使用 Block 來實現匿名函式,由於 Block 的閉包性質,也可以使用它來實現引數傳遞。
首先在 LanguageVC 中定義一個 Block 屬性:
@property (nonatomic, copy) void (^languageSelectionBlock)(NSUInteger index);
然後在使用者選擇的時候呼叫 Block 方法:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if (self.languageSelectionBlock) {
self.languageSelectionBlock(indexPath.row);
}
}
在 SettingVC 中,不再需要實現 delegate 方法,直接進行 block 賦值即可:
LanguageViewController *vc = [[LanguageViewController alloc] initWithIndex:self.currentSelectedIndex];
vc.languageSelectionBlock = ^(NSUInteger index){
// Do something
};
優點:可以看到,和 delegate 相比,block 使用起來語法更加簡明方便,不需要定義 protocol。同時 block 本身是一個物件,是可以進行傳遞的,用法比 delegate 要靈活很多。
缺點:由於 block 會捕獲外部的物件,使用 block 很容易造成記憶體洩露問題,需要多加小心。同時 block 層次過多也會使得回撥層數太深,導致程式碼難以維護。
其他方法
除了上面提到的方法,iOS 系統內部還提供了幾種用於資料共享和傳遞的機制:
NSNotification
NSNotification 是 iOS 自帶的一個通知機制,物件可以註冊和監視通知,可以以通知中心為中介,實現資訊的傳送和接受。
優點: NSNotification 支援多播,即一個通知被多個物件接收,這一點是上面的幾種方式所不能提供的,另一方面,NSNotification 是全域性性質的,因此不會受到 VC 層次的影響,很適合跨 VC 的訊息傳遞。
缺點: NSNotification 的優點也正是它的缺點。由於它多播以及全域性的性質,很容易造成通知的汙染,因此在程式設計中要注意不要接收自己不感興趣的通知。
KVO
KVO(Key-Value Observing)是系統自帶的一種訊息傳遞機制,可以實現物件之間對某個屬性值的觀察。
優點: KVO 可以對屬性值實現細粒度的觀察,這一點是別的方法不容易做到的,也是很有用的一個特性。
缺點: KVO 語法使用很繁瑣,基於字串對值進行觀察本身也很容易出錯。同時由於 KVO 本身的實現基於 Runtime,對於原始型別(struct 等)是沒有用處的( NSObject 中的 struct 等屬性可以進行 KVO,因為系統會自動處理裝箱和拆箱操作,這是說的是非 NSObject 物件),這一點也限制了 KVO 的應用場景。
除此之外,使用 NSUserDefaults 或者 AppDelegate 本身也能實現資料的傳遞,不過在設計上和上面介紹的幾種方法相比,並沒有什麼獨特的優勢,在這裡就不再贅述了。