1. 程式人生 > >NSNotificationCenter 通知使用方法詳解

NSNotificationCenter 通知使用方法詳解

你要知道的KVC、KVO、Delegate、Notification都在這裡

本系列文章主要通過講解KVC、KVO、Delegate、Notification的使用方法,來探討KVO、Delegate、Notification的區別以及相關使用場景,本系列文章將分一下幾篇文章進行講解,讀者可按需查閱。

NSNotificationCenter 通知的使用方法詳解

NSNotificationCenter通知中心是iOS程式內部的一種訊息廣播的實現機制,可以在不同物件之間傳送通知進而實現通訊,通知中心採用的是一對多的方式,一個物件傳送的通知可以被多個物件接收,這一點與我們前面講解的KVO

機制類似,KVO觸發的回撥函式也可以被對個物件響應,但代理模式delegate則是一種一對一的模式,委託物件只能有一個,物件也只能和委託物件通過代理的方式通訊。

首先看一下比較重要的NSNotification類,這是通知中心的基礎,通知中心傳送的的通知都會封裝成該類的物件進而在不同物件之間傳遞。其比較重要的屬性和方法如下:

//通知的名稱,有時可能會使用一個方法來處理多個通知,可以根據名稱區分
@property (readonly, copy) NSNotificationName name;
//通知的物件,常使用nil,如果設定了值註冊的通知監聽器的object需要與通知的object匹配,否則接收不到通知
@property (nullable, readonly, retain) id object; //字典型別的使用者資訊,使用者可將需要傳遞的資料放入該字典中 @property (nullable, readonly, copy) NSDictionary *userInfo; //下面三個是NSNotification的建構函式,一般不需要手動構造 - (instancetype)initWithName:(NSNotificationName)name object:(nullable id)object userInfo:(nullable NSDictionary *)userInfo API_AVAILABLE(macos(10.6
), ios(4.0), watchos(2.0), tvos(9.0)) NS_DESIGNATED_INITIALIZER; + (instancetype)notificationWithName:(NSNotificationName)aName object:(nullable id)anObject; + (instancetype)notificationWithName:(NSNotificationName)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo;

NSNotification通知類本身很簡單,需要著重理解的就是其三個屬性,接下來看一下NSNotificationCenter通知中心,通知中心採用單例的模式,整個系統只有一個通知中心,通過如下程式碼獲取:

[NSNotificationCenter defaultCenter]

再看一下通知中心的幾個核心方法:

/*
註冊通知監聽器,只有這一個方法
observer為監聽器
aSelector為接到收通知後的處理函式
aName為監聽的通知的名稱
object為接收通知的物件,需要與postNotification的object匹配,否則接收不到通知
*/
- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSNotificationName)aName object:(nullable id)anObject;

/*
傳送通知,需要手動構造一個NSNotification物件
*/
- (void)postNotification:(NSNotification *)notification;

/*
傳送通知
aName為註冊的通知名稱
anObject為接受通知的物件,通知不傳參時可使用該方法
*/
- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject;

/*
傳送通知
aName為註冊的通知名稱
anObject為接受通知的物件
aUserInfo為字典型別的資料,可以傳遞相關資料
*/
- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo;

/*
刪除通知的監聽器
*/
- (void)removeObserver:(id)observer;

/*
刪除通知的監聽器
aName監聽的通知的名稱
anObject監聽的通知的傳送物件
*/
- (void)removeObserver:(id)observer name:(nullable NSNotificationName)aName object:(nullable id)anObject;

/*
以block的方式註冊通知監聽器
*/
- (id <NSObject>)addObserverForName:(nullable NSNotificationName)name object:(nullable id)obj queue:(nullable NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0));

接下來舉一個栗子,和之前delegate的栗子相同,只不過這裡使用通知來實現,依舊是兩個頁面,ViewControllerNextViewController,在ViewController中有一個按鈕和一個標籤,點選按鈕跳轉到NextViewController檢視中,NextViewController中包含一個輸入框和一個按鈕,使用者在完成輸入後點擊按鈕退出檢視跳轉回ViewController並在ViewController的標籤中展示使用者填寫的資料,接下來看一下程式碼:

//ViewController部分程式碼

- (void)viewDidLoad
{
    //註冊通知的監聽器,通知名稱為inputTextValueChangedNotification,處理函式為inputTextValueChangedNotificationHandler:
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(inputTextValueChangedNotificationHandler:) name:@"inputTextValueChangedNotification" object:nil];

}

//按鈕點選事件處理器
- (void)buttonClicked
{
    //按鈕點選後建立NextViewController並展示
    NextViewController *nvc = [[NextViewController alloc] init];
    [self presentViewController:nvc animated:YES completion:nil];
}

//通知監聽器處理函式
- (void)inputTextValueChangedNotificationHandler:(NSNotification*)notification
{
    //從userInfo字典中獲取資料展示到標籤中
    self.label.text = notification.userInfo[@"inputText"];
}

- (void)dealloc
{
    //當ViewController銷燬前刪除通知監聽器
    [[NSNotificationCenter defaultCenter] removeObserver:self name:@"inputTextValueChangedNotification" object:nil];
}


//NextViewController部分程式碼
//使用者完成輸入後點擊按鈕的事件處理器
- (void)completeButtonClickedHandler
{
    //傳送通知,並構造一個userInfo的字典資料型別,將使用者輸入文字儲存
    [[NSNotificationCenter defaultCenter] postNotificationName:@"inputTextValueChangedNotification" object:nil userInfo:@{@"inputText": self.textField.text}];
    //退出檢視
    [self dismissViewControllerAnimated:YES completion:nil];
}

程式碼比較簡單不再給出相關執行截圖了,不難發現NSNotificationCenter的使用步驟如下:

  • 1、在需要監聽某通知的地方註冊通知監聽器

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(inputTextValueChangedNotificationHandler:) name:@"inputTextValueChangedNotification" object:nil];
  • 2、實現通知監聽器的回撥函式

    - (void)inputTextValueChangedNotificationHandler:(NSNotification*)notification
    {
        self.label.text = notification.userInfo[@"inputText"];
    }
  • 3、在監聽器物件銷燬前刪除通知監聽器

    - (void)dealloc
      {
          [[NSNotificationCenter defaultCenter] removeObserver:self name:@"inputTextValueChangedNotification" object:nil];
      }
  • 4、如有通知需要傳送,使用NSNotificationCenter傳送通知

    [[NSNotificationCenter defaultCenter] postNotificationName:@"inputTextValueChangedNotification" object:nil userInfo:@{@"inputText": self.textField.text}];

對於刪除監聽器這一步驟在iOS9以後似乎變得不那麼重要,iOS9開始不再對已經銷燬的監聽器傳送通知,當監聽器物件銷燬後傳送通知也不會造成野指標錯誤,這一點比KVO更加安全,KVO在監聽器物件銷燬後仍會觸發回撥函式就可能造成野指標錯誤,因此使用通知也就可以不手動刪除監聽器了,但如果需要適配iOS9之前的系統還是需要養成手動刪除監聽器的習慣。

上面的栗子很簡單,但有一點是需要強調的,我們在NextViewController中傳送的通知是在main執行緒中傳送的,因此ViewController中的監聽器回撥函式也會在main執行緒中執行,因此我們在監聽器回撥函式中修改UI不會產生任何問題,但當通知是在其他執行緒中傳送的,監聽器回撥函式很有可能就是在傳送通知的那個執行緒中執行,我們知道UI的更新必須在主執行緒中執行,這個時候就需要注意,如果通知監聽器回撥函式有需要更新UI的程式碼,需要使用GCD放在主執行緒中執行,程式碼如下:

//NextViewController傳送通知的程式碼修改為如下程式碼:
- (void)completeButtonClickedHandler
{
    //使用GCD獲取一個非主執行緒的執行緒用於傳送通知
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [[NSNotificationCenter defaultCenter] postNotificationName:@"inputTextValueChangedNotification" object:nil userInfo:@{@"inputText": self.textField.text}];
    });

    [self dismissViewControllerAnimated:YES completion:nil];
}

//ViewController通知監聽器的回撥函式修改為如下程式碼:
- (void)inputTextValueChangedNotificationHandler:(NSNotification*)notification
{
    //使用GCD獲取主執行緒並更新UI
    dispatch_async(dispatch_get_main_queue(), ^{
        self.label.text = notification.userInfo[@"inputText"];
    });
    //如果不在主執行緒更新UI很有可能無法正確執行
    //self.label.text = notification.userInfo[@"inputText"];
}

很多時候我們使用的是第三方框架傳送的通知,或是系統提供的通知,我們無法預知這些通知是否是在主執行緒中傳送的,為了安全起見最好在需要更新UI時使用GCD將更新的邏輯放入主執行緒執行。

系統提供了很多各式各樣的通知,比如當我們要實現IM即時通訊類app的聊天頁面輸入框時就可以使用系統鍵盤發出的通知,相關通知有UIKeyboardWillShowNotificationUIKeyboardWillHideNotification,顧名思義一個是鍵盤即將展示,一個是鍵盤即將退出的通知,接下來給一個簡單的實現:

#import "ViewController.h"

#define ScreenWidth [[UIScreen mainScreen] bounds].size.width
#define ScreenHeight [[UIScreen mainScreen] bounds].size.height

@interface ViewController ()

@property (nonatomic, strong) UIView *containerView;
@property (nonatomic, strong) UITextField *textField;

@end

@implementation ViewController

@synthesize containerView = _containerView;
@synthesize textField = _textField;

- (instancetype)init
{
    if (self = [super init])
    {
        self.view.backgroundColor = [UIColor whiteColor];

        //建立一個容器View可自定義相關UI
        self.containerView = [[UIView alloc] initWithFrame:CGRectMake(0, ScreenHeight - 60, ScreenWidth, 60)];
        self.containerView.backgroundColor = [UIColor redColor];
        [self.view addSubview:self.containerView];

        //使用者輸入的UITextField
        self.textField = [[UITextField alloc] initWithFrame:CGRectMake(20, 10, ScreenWidth - 40, 40)];
        self.textField.placeholder = @"input...";
        self.textField.backgroundColor = [UIColor greenColor];
        [self.containerView addSubview:self.textField];

        [self.view addSubview:self.containerView];

        //新增一個手勢點選空白部分後收回鍵盤
        UIGestureRecognizer *gesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapView)];
        [self.view setMultipleTouchEnabled:YES];
        [self.view addGestureRecognizer:gesture];

    }
    return self;
}

- (void)viewDidLoad
{
    //註冊UIKeyboardWillShowNotification通知,監聽鍵盤彈出事件
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
    //註冊UIKeyboardWillHideNotification通知,監聽鍵盤迴收事件
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
}

//自定義手勢響應處理器
- (void)tapView
{
    //觸發收回鍵盤事件
    [self.textField resignFirstResponder];
}

//UIKeyboardWillShowNotification通知回撥函式
- (void)keyboardWillShow:(NSNotification*)notification
{
    //獲取userInfo字典資料
    NSDictionary *userInfo = [notification userInfo];
    //根據UIKeyboardBoundsUserInfoKey鍵獲取鍵盤高度
    float keyboardHeight = [[userInfo objectForKey:@"UIKeyboardBoundsUserInfoKey"] CGRectValue].size.height;
    //獲取鍵盤彈出的動畫時間
    NSTimeInterval animationDuration;
    [[userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey] getValue:&animationDuration];
    //自定義動畫修改ContainerView的位置
    [UIView animateWithDuration:animationDuration animations:^{
        self.containerView.frame = CGRectMake(0, ScreenHeight - keyboardHeight - self.containerView.frame.size.height, self.containerView.frame.size.width, self.containerView.frame.size.height);
    }];

}

//UIKeyboardWillHideNotification通知回撥函式
- (void)keyboardWillHide:(NSNotification*)notification
{
    //獲取動畫執行執行時間
    NSValue *animationDurationValue = [[notification userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey];
    NSTimeInterval animationDuration;
    [animationDurationValue getValue:&animationDuration];
    //自定義動畫修改ContainerView的位置
    [UIView animateWithDuration:animationDuration animations:^{
        self.containerView.frame = CGRectMake(0, ScreenHeight - self.containerView.frame.size.height, self.containerView.frame.size.width, self.containerView.frame.size.height);
    }];
}

- (void)dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillShowNotification object:nil];
    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillHideNotification object:nil];
}

@end

最終效果如下圖所示(GIF下載可能有點慢):

絲滑鍵盤

備註

由於作者水平有限,難免出現紕漏,如有問題還請不吝賜教。