Notificationの使い方に関してはググればいくらでも出てくると思いますが、ここでは実戦向けに少しだけ掘り下げて説明出来ればと思います。

アプリケーション独自のNotification関連を定義するファイル

アプリケーションのプレフィックス(大文字2文字)+Notificationという名前のファイルにNotification関連の定義をまとめておくと良いでしょう。

今回はSTNotification.hとSTNotification.mというファイルにまとめることにします。

Notification名の定義

SDKや他のライブラリの名前と重複しないようにNotification名をつける必要があります。アプリケーションのプレフィックス+Notification名+Notificationという命名規則にすると良いでしょう。

今回は以下のようなNotificationを定義します。

STNotification.h

UIKIT_EXTERN NSString *const STSentMessageNotification;
UIKIT_EXTERN NSString *const STReceivedMessageNotification;

STNotification.m

NSString *const STSentMessageNotification = @"STSentMessageNotification";
NSString *const STReceivedMessageNotification = @"STReceivedMessageNotification";

#defineより定数として定義したほうが型がはっきりして良さそうです。実際、SDKで定義されたNotificationは定数です。また定数はXcodeでコード補完が効くので便利です。

Notificationと一緒に送るパラメータ

通知と一緒にパラメータを送りたい時もあるでしょう。そのためのクラスを定義します。命名規則は先ほど定義したNotification定数名+Parametersで統一します。

@interface STSentMessageNotificationParameters : NSObject

@property (strong, nonatomic) NSString *tweet;
@property (nonatomic) NSUInteger statusCode;

@end

@interface STReceivedMessageNotificationParameters : NSObject

@property (strong, nonatomic) NSString *from;
@property (strong, nonatomic) NSString *message;

@end

Notificationの送信

Notificationを送信するには以下のようにします。

  
STReceivedMessageNotificationParameters *parameters = [[STReceivedMessageNotificationParameters alloc] init];
parameters.from = @"test@example.com";
parameters.message = @"This is test.";
NSDictionary *userInfo = @{@"parameters": parameters};

NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter postNotificationName:STReceivedMessageNotification object:self userInfo:userInfo];

NSNotificationCenter#postNotificationを使って送信します。パラメータはuserInfoに詰めます。引数objectは通常、送り手を設定しますがnilでも構いません。またobjectにパラメータオブジェクトを渡してしまう手もありますが、その辺の考察については最後に述べます。

userInfoにパラメータを毎回詰めるのは面倒なので、NSNotificationに以下の様なカテゴリメソッドを作ると楽です。

NSNotification+Parameters.h

@interface NSNotification (Parameters)

+ (id)notificationWithName:(NSString *)name object:(id)object parameters:(id)parameters;
- (id)parameters;

@end

NSNotification+Parameters.m

#import "NSNotification+Parameters.h"

@implementation NSNotification (Parameters)

+ (id)notificationWithName:(NSString *)name object:(id)object parameters:(id)parameters
{
    NSDictionary *userInfo = @{@"parameters" : parameters};
    return [self notificationWithName:name object:object userInfo:userInfo];
}

- (id)parameters
{
    return [self.userInfo objectForKey:@"parameters"];
}

@end

このようにしておくと先ほどのNotification送信は以下のように書き換えられます。

  
STReceivedMessageNotificationParameters *parameters = [[STReceivedMessageNotificationParameters alloc] init];
parameters.from = @"test@example.com";
parameters.message = @"This is test.";

NSNotification *notification = [NSNotification notificationWithName:STReceivedMessageNotification object:self parameters:parameters];

NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter postNotification:notification];

Notification受け取り側でのparameters取り出しもスムーズなので、この方法を推奨します。userInfoの各keyにゴテゴテとパラメータを詰むのはメンテナンス性が悪いのでおすすめしません。

Notificationの受信

多くの場合、ViewControllerで通知を受け取ることになると思いますが、viewDidAppear,viewDidDisappearで受信と受信解除に関する処理を行うと良いのではと思います。

- (void)viewDidAppear:(BOOL)animated
{
    NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
    [notificationCenter addObserver:self selector:@selector(handleReceivedMessageNotificationObject:) name:STReceivedMessageNotification object:nil];
    [notificationCenter addObserver:self selector:@selector(handleSentMessageNotificationObject:) name:STSentMessageNotification object:nil];
}

- (void)viewDidDisappear:(BOOL)animated
{
    NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
    [notificationCenter removeObserver:self];
}

NSNotification#addObserverでselfを渡すことでViewController自身が通知の受け取り先になります。selector引数で通知を受け取るメソッド渡します。ここのobject引数は通常nilです。

次に通知を受け取るメソッドをメソッドの実装です。

- (void)handleReceivedMessageNotification:(NSNotification *)notification
{
    STReceivedMessageNotificationParameters *parameters = notification.parameters;

    // Do something.
}

- (void)handleSentMessageNotificationObject:(NSNotification *)notification
{
    STSentMessageNotificationParameters *parameters = notification.parameters;

    // Do something.
}

NSNotification#objectから送信時に指定したNotificationObjectを取得できます。

viewDidAppear/viewDidDisapper以外で受信設定する場合

場合によっては次の画面に遷移している間も、Notificationを受け取りたい場合があります。その場合は、以下のようにすると良いでしょう。

  • viewDidLoadでNSNotification#addObserverして受信可能にする
  • viewDidUnloadとdeallocでremoveObserverして受信解除する

状況によりますが基本はviewが作られた後にaddObsereverしたほうが良いと思います。

viewDidUnloadはメモリが足らなくなってviewを解放するときにだけ呼ばれるので、deallocでもremoveObserverする必要があります。逆にdeallocだけremoveObserverするようにしていると、以下のようにバグの原因になります。

  • viewDidLoad (addObserverを呼ぶ)
  • 他の画面に遷移
  • メモリが足らなくなる
  • viewDidUnload
  • この画面に戻ってきて再表示
  • viewDidLoad (addObserverを呼ぶ)
  • この時点でremoveObserverが呼ばれることなくaddObserverが2回呼ばれて2重登録されている

つまり同じ通知を2回受け取ることになります。この辺は注意が必要です。

ではviewDidAppear/viewDidDisappearでは駄目な場合はどういったケースが考えられるでしょうか?

メールクライアントを例にしましょう。メールの一覧画面ではメールが削除されたら、そのメールは一覧から消える必要があります。このメールクライアントでは、一覧画面以外でも削除される可能性があるとします。

一覧画面から詳細画面へ遷移して詳細画面に削除ボタンがあって削除できる。それくらいだと一覧画面と詳細画面の間でDelegateを使えば事足りますが、メールの削除方法にいろいろなパターンがあるとそうはいきません。そうすると一覧画面のViewControllerは表示されていなくとも常にNotificationを監視する必要があります。

実際にアプリケーションの規模が大きくなるとこういった状況は起き得ます。状況に応じてaddObserver/removeObserverを呼び出すタイミングを考える必要があります。

メッセージの通知をBlockで受け取る方法

メッセージの通知をBlockで受け取る方法もあります。以下はDocumentからの引用。

NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
self.localeChangeObserver = [center addObserverForName:NSCurrentLocaleDidChangeNotification object:nil
    queue:mainQueue usingBlock:^(NSNotification *note) {

        NSLog(@"The user's locale changed to: %@", [[NSLocale currentLocale] localeIdentifier]);
    }];

引数でObserverを指定するのではなく、戻り値としてObserverを返すのが異なるところです。もちろん通知を受け取る必要がなくなったらremoveObserverする必要があります。

NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center removeObserver:self.localeChangeObserver];

Blockは便利ではあるんですが、受け取るNotificationが複数あってaddObserverを複数呼び出している時、removeObserverはselfだけ渡して1回で済ませたい時があります。Blockを使うとaddObserverした分、removeObsereverせねばならず、ちょっと面倒ですね。自分はNotificationについてはBlockは使わない方針です。

NSNotification#objectにパラメータを渡しても良いか?

userInfoにいちいちパラメータを詰める必要は無い気もします。SDK DocumentにはNSNotification#objectについて以下のように書かれています。

The object associated with the notification. This is often the object that posted this notification. It may be nil.

Notificationに関するオブジェクト。しばしばNotificationを送信したオブジェクトである。そのように書かれていますね。ちなみにoften(しばしば)は60%くらいの確率を指すそうです(笑)。よってアプリケーションレベルではobjectにパラメータを渡しても良さそうです。

ただし、NSNotificationCenter#postNotificationName:object:のDocumentでは、object引数の変数名はnotificationSenderとなっています。

※ このメソッドはNSNotification#notificationName:object:を内部で呼び出し、NSNotificationを生成します。

さらにobject(notificationSener)について以下のように書かれています。

The object posting the notification.

こちらでは、はっきりとNotificationの送り手と書かれていますね・・・ただこのメソッドはエイリアスみたいなものですし、NSNotificationの方が信頼度が高い気がします。はっきり用途を決めていないのかもしれません。開発者個人の判断で良いのではないでしょうか。

自分はカテゴリメソッドを作れば特に気にならないのでuserInfoにパラメータを詰めることにしました。

ちなみにNSNotificationCenter#addObserverでobject引数にnil以外を渡すと、NSNotificaion#objectがそれと一致するものしか受信しないようにできます。つまり受信フィルタリングができます。この仕組も考慮するとobject引数は送信者であるべきかもしれません。しかし、この仕組みを使うことはあまりないように思います。受信者が送信者を知っているならDelegateでやりとりしたほうが効率的ですから。

ただしSDKレベルのNotificationはこのフィルタリングが有効かもしれません。たまになんでDelegateじゃなくNotificationなのっていうケースがありますし。UITextFieldTextDidChangeNotificationtとか・・・きちんとフィルタリングしておかないと他の画面のテキスト変更を受け取ってしまうかもしれません。