【前言介紹】
iOS的一種設計模式,觀察者Observer模式(也叫發布/訂閱,即Publich/Subscribe模式)。
觀察者模式,包含了通知機制(notification)和KVO(Key-value-observing)機制。
在這本文中,我們將介紹在日常項目當中經常使用到的通知機制這一種設計模式。
通知機制
委托機制是代理“一對一”的對象之間的通信,而通知機制是廣播“一對多”的對象之間的通信;
一、是什么?【生活問題例子】
“短信天氣預報”
當A類發送一條信息給通知中心時,注冊為用戶(觀察者)的B類群就會收到相應的通知,並作出反應。
二、有什么用?【代碼中的應用】
在不同類之間如何傳遞數據?
有幾種方法:屬性傳遞、代理協議,另外就是通知。
通知:在A類中創建的方法,B類中執行,且可以使用該通知攜帶數據傳遞給對方;
三、有什么不同?【與其他“通知”的不同?】
經常提到的通知,有“廣播通知”、“本地通知”、“推送通知”
本文所介紹的就是廣播通知,是實現觀察者模式的一種機制,可以在一個應用中的多個對象之間進行通信傳遞數據。
而本地通知和推送通知主要是給用戶發送“通知提示”,例如警告提示、聲音、震動以及如圖標上的紅色數字提示。
第一種由“本地發送通知”給用戶,第二種由第三方應用發送給蘋果官方的遠程服務器,然后再由服務器“推送通知”給用戶。
四、產品經理:老規矩,代碼拿來~【具體實現】
過程:
- 在通知機制中,需要(或者說感興趣)接收某個通知的信息的所有對象都可以成為接收者,首先注冊成為觀察者。
- 進行注冊后,通知中心就會把發布者發送的通知信息,廣播給注冊過該通知的觀察者。且觀察者只能接收到通知中心的信息,不能知道通知是誰投送的。
- 最后,接受者不想再對關注該通知的信息時,可以給通知中心發生解除注冊的信息,之后都不再接收到通知了。
1.獲取通知中心(NSNotificationCenter)對象:(就像獲取移動營運商短信中心的權限,作為媒介)
發布、注冊、解除通知都需要使用通知中心,負責協助不同對象、不同類之間的消息通信。
[NSNotificationCenter defaultCenter]; //需要注意的是,通知中心也是一個單例
2.發布(A類)和接收(B類)
a.做為發布者的A類發送通知:
可以使用一下三個方法:
- (void)postNotification:(NSNotification *)notification; - (void)postNotificationName:(NSString *)aName object:(id)anObject; - (void)postNotificationName:(NSString *)aName object:(id)anObject userInfo:(NSDictionary *)aUserInfo;
- postNotificationName:指定消息名稱;
- object:指定發消息者;
- userInfo:通知中用於傳遞參數的載體,傳遞的方法是把參數放在NSDictionary類型的userInfo中。例如:NSDictionary *dict = [notification userInfo];
一般使用第三個方法,如果參數不需要的,可以設置為nil.
b.注冊通知,加入觀察者:
做為觀察者B類注冊通知,進行監聽:
- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSString *)aName object:(nullable id)anObject;
//@selector中為回調方法,在本類中對通知進行相應的處理,name為通知名稱、object為對象;
剖析:
- object == nil,那么客戶對象(self)將收到任何對象發出NSWindowDidBecomeMainNotification的通知消息;
- name == nil,那么觀察者將接收到object對象的所有消息,但是確定不了接收這些消息的順序。
- object == nil,name == nil,那么該觀察者將收到所有對象的所有消息。
對於一個任意的觀察者observer,如果不能保證其對應的selector有本類自定義的方法(例如:MyMethod),可采用[observer respondsToSelector:@selector(MyMethod:)]] 進行檢查。
所以完整的添加觀察者過程為:
if([observer respondsToSelector:@selector(MyMethod:)]) {
[[NSNotificationCenter defaultCenter] addObserver:observer selector:@selector(MyMethod:) name:NSWindowDidBecomeMainNotification object:nil];
}
當然在蘋果API中也有另外一個注冊觀察者的方法:
- (id <NSObject>)addObserverForName:(nullable NSString *)name object:(nullable id)obj queue:(nullable NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block
此方法是支持在該方法中進行block回調的,而queue參數就是表示此模塊在queue隊列中進行。
但是這方法一般不采用,因為在此方法的 block 中,稍微不注意調用 self 的話,會引起循環引用,造成內存泄露,所以還是建議使用第一種方法進行觀察者的創建。
c.移除通知
注冊過的對象必須在釋放之前注銷掉,如果不這樣的話,當該通知再次出現時,通知中心可能會向已釋放的觀察者對象發送消息,從而導致應用崩潰。
在ARC下,系統會自動回收無用的通知對象內存,但是由於系統回收機制ARC有一定的延遲性,所以即使不會出錯,也建議養成習慣,對通知進行手動釋放無用的通知。
移除有2種方法:
//釋放所有通知
- (void)removeObserver:(id)observer;
//釋放名稱為aName的通知 - (void)removeObserver:(id)observer name:(nullable NSString *)aName object:(nullable id)anObject;
一般在視圖控制器中,可以在“didReceiveMemoryWarning:”中發送解除消息:【這只是參考,建議還是在 :-(void)dealloc ){} 中進行移除。 】
-(void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
//移除觀察者
[[NSNotificationCenter defaultCenter]removeObserver:self];
}
(by:從 iOS 9 開始通知中心會對觀察者進行 weak 弱引用,所以不需要在觀察者對象釋放之前從通知中心移除。即使不對通知進行手動移除,指針也會在注冊者被回收后自動置空,向空指針 nil 發送消息是不會有問題的。
但是,通過 - (id )addObserverForName: object: queue: usingBlock:
方法注冊的觀察者依然需要手動的釋放,因為通知中心對它們持有的是強引用。)
五、那些年我們用過的系統通知名稱~
系統自帶的也有許多有用的通知,我們只需要注冊為相應的通知接收對象,就能根據通知狀態的變化發生相應的數據改變。
部分系統通知名稱如下:
UIApplicationDidFinishLaunchingNotification // 應用程序啟動后 UIApplicationDidBecomActiveNotification // 進入前台 UIApplicationWillResignActiveNotification // 應用將要進入后台
UIApplicationDidEnterBackgroundNotification // 進入后台 UIKeyboardWillShowNotification // 鍵盤即將顯示 UIKeyboardDidShowNotification // 鍵盤顯示完畢 UIKeyboardWillHideNotification // 鍵盤即將隱藏 UIKeyboardDidHideNotification // 鍵盤隱藏完畢
六、舉個栗子:“🌰”
本文有2個例子:
- 一個是完整的通知發布、接收、解除過程;
- 一個是系統通知名稱的應用(以第三個:UIApplicationWillResignActiveNotification
為例);
(by:覺得文章太長不想看這段的童鞋,也可以到github上下載啊左的demo,自己琢磨琢磨:Mydemo1、 Mydemo2。
點擊“DownLoad ZIP”按鈕就可以了。一般使用Safari瀏覽器下載得了,啊左用QQ瀏覽器貌似下載不了...囧)
【本次開發環境: Xcode:7.2 iOS Simulator:iphone6 By:啊左】
1.完整的通知發布、接收、解除過程:
UI控件擺放如下,視圖、控件的背景可以自己設置成比較明顯的顏色,便於觀察:
A視圖創建一個textView用於顯示B視圖傳遞過來的信息,一個按鈕用於切換到B視圖;
B視圖創建一個文本框用於更新信息,一個按鈕用於把文本框的信息更新並返回到視圖A。
然后,點擊A類的按鈕,並且按住control拖拽到B視圖的控制器后松開鼠標,在彈出的選擇框(如下圖)選擇:“Present Modally”用於創建A、B控制器之間的模態類型的Segue。
接下來,我們需要在新建一個視圖控制器B類SeocndViewController:
回到故事板中,選擇B視圖控制器,打開標識檢查器(下圖第一排第三個選項),選擇class為:SeocndViewController。這就使代碼與故事板中的視圖控制器對應起來。(A視圖默認對應ViewController,如果有錯誤,可以檢查一下。)
然后我們打開輔助編輯器,按住control,拖拽A視圖中的文本連接到對應的輸出口,這里我們命名為“myLabel”.
以此方式,繼續為B類中的文本框連接到代碼中,並命名為:“MyTextView”,
為B類的按鈕添加行為,方法名為:“saveBtn:”,
啊左還是覺得上代碼實在點:
(ViewController.h)
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController
@property (weak, nonatomic) IBOutlet UITextView *myLabel; //每次視圖打開后,監聽B類的數據是否發生變化,如有變化,在這個文本視圖中顯示更新。
@end
(ViewController.m)
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//1.注冊為觀察者,監聽B視圖中的通知
[[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(AMethod:) name:@"MyNotificationName" object:nil];
}
-(void)AMethod:(NSNotification *)notification
{
//2.獲取通知攜帶的數據,更新label的文本信息
NSDictionary *dictData = [notification userInfo];
NSString *str = [dictData objectForKey:@"MyUserInfoKey"];
self.myLabel.text = str;
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
//3.移除所有通知
[[NSNotificationCenter defaultCenter]removeObserver:self];
}
@end
(SecondViewController.h)
#import <UIKit/UIKit.h>
@interface SecondViewController : UIViewController
@property (weak, nonatomic) IBOutlet UITextField *MyTextView; //文本框,用於更新傳遞給ViewController視圖的數據
- (IBAction)saveBtn:(UIButton *)sender; //保存返回按鈕事件
@end
(SecondViewController.m)
#import "SecondViewController.h"
@interface SecondViewController ()
@end
@implementation SecondViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
- (IBAction)saveBtn:(UIButton *)sender {
//返回視圖A並發布通知
[self dismissViewControllerAnimated:YES completion:^{
//1.創建userInfo攜帶的信息
NSString *str = self.MyTextView.text;
NSDictionary *dictData = [NSDictionary dictionaryWithObject:str forKey:@"MyUserInfoKey"];
//2.發布信息
[[NSNotificationCenter defaultCenter]postNotificationName:@"MyNotificationName" object:nil userInfo:dictData];
}];
}
@end
驗證:
第一次A視圖的文本視圖中沒有顯示數據,點擊按鈕“確定切換頁面”,打開視圖B,在文本框中輸入信息(例如123),點擊“保存返回”按鈕,在A視圖的文本視圖中看到更新的信息:123。
by:有需要的童鞋可以到github上下載啊左的demo:Mydemo1。
2.系統通知名稱的應用(以UIApplicationWillResignActiveNotification為例):
UIApplicationWillResignActiveNotification的意思是應用即將進入后台的這個時刻。
首先,創建UI界面如下,相比第一個例子,這個會簡單很多:一個按鈕+一個顯示顏色的UIView視圖。
創建一個命名為“myView”的UIView控件,一個方法為“changeColorBtn:”的按鈕行為即可,關聯ViewController控制器。
代碼如下:
(ViewController.h)
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController
@property (weak, nonatomic) IBOutlet UIView *myView;
- (IBAction)changeColorBtn:(UIButton *)sender;
@end
(ViewController.m)
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//1. 注冊為觀察者
[[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(applicationWillResignActiveNotification:) name:UIApplicationWillResignActiveNotification object:nil];
}
//2.當應用即將進入后台時,調用通知回調方法:
-(void)applicationWillResignActiveNotification:(
NSNotification *)notification
{
//返回后台的過程,把視圖背景改為紅色;
self.myView.backgroundColor = [UIColor redColor];
}
- (IBAction)changeColorBtn:(UIButton *)sender {
//按鈕把視圖背景改為黃色;
self.myView.backgroundColor = [UIColor yellowColor];
}
@end
視圖第一次打開,視圖為默認白色:
點擊按鈕,視圖變為黃色:
按住“command+shift”,雙擊H,進入iOS多任務欄;
或者按住“command+shift”,單擊H,回到模擬器主界面。
都可以看到視圖變為紅色。
且回到應用后,顏色仍然是紅色。
表示,當應用從活躍的狀態進入非活躍狀態的時候,系統自動發送“UIApplicationWillResignActiveNotification”這個通知,如有注冊監聽者(觀察者),則執行回調方法。
by:有需要的童鞋可以到github上下載啊左的demo:Mydemo2。