addObserverForName:object:queue:usingBlock:の罠

iOS 4からNSNotificationCenterに追加されたaddObserverForName:object:queue:usingBlock:メソッドを使うと、コールバックをブロックで指定することができるのでとても便利です。しかし、従来のセレクタ指定型のaddObserver:selector:name:object:のブロック版、という認識で使ってはいけません。

addObserver:とremoveObserver:のペア

例えば、ビューコントローラが全面に出ているときは通知を受け取りたいが、popされたり他のビューコントローラが上にきた時は通知を受け取りたくない、というようなパターンはNSNotificationCenterを使うとスマートに実装できます。
ビューコントローラのviewDidAppearでaddObserver:…を使って自身をオブザーバーとして登録して、viewWillDisappearでremoveObserver:を使ってオブザーブを解除すれば、そのビューコントローラが前面にあるときのみ、通知を受け取ることができます。

- (void)viewDidAppear {
  // この画面が表示されている間にアプリが切り替わったら下記の処理が走る
  [[NSNotificationCenter defaultCenter]
    addObserver:self
    selector:@selector(applicationDidEnterBackground:)
    name:UIApplicationDidEnterBackgroundNotification
    object:nil
  ];
  [[NSNotificationCenter defaultCenter]
    addObserver:self
    selector:@selector(applicationWillEnterForeground:)
    name:UIApplicationWillEnterForegroundNotification
    object:nil
  ];
}
- (void)viewWillDisappear {
  // 画面が消えたら、この画面用のアプリきりかえ時のコードも走らないようにする
  [[NSNotificationCenter defaultCenter] removeObserver:self];
}

removeObserver:を呼び出した場合、そのオブザーバーに結びついているすべてのオブザーブを一気に解除できるので、表にいるときだけ複数のNotificationをオブザーブしたいような場合にはきわめて便利です。オブザーバー自身で初期化時にaddObserver:でオブザーバー登録を行い、破棄時にremoveObserver:でオブザーバー解除を行う組み合わせはしばし登場してきたパターンです。

addObserver:をaddObserverForName:にそのまま置き換えてはいけない

addObserver:では引数でオブザーバーを指定します。一方addObserverForName:では引数でオブザーバーの指定は行わず、ブロックだけを指定します。

// selfがオブザーバーになる
- (void)viewDidAppear {
  [[NSNotificationCenter defaultCenter]
    addObserver:self
    selector:@selector(applicationDidEnterBackground:)
    name:UIApplicationDidEnterBackgroundNotification
    object:nil
  ];
}
- (void)applicationDidEnterBackground:(NSNotification*)note {
  //TODO:
}
----
// selfがオブザーバーにならない
- (void)viewDidAppear {
  [[NSNotificationCenter defaultCenter]
    addObserverForName:UIApplicationDidEnterBackgroundNotification
    object:nil queue:[NSOperationQueue mainQueue]
    usingBlock:^(NSNotification *note) {
      //TODO:
    }
  ];
}

従って、addObserver:をaddObserverForName:に単純に置き換えると、removeObserver:で引数に元々のオブザーバーを指定したところで、オブザーブの解除は行われなくなってしまいます。

addObserverForName:を使う場合は返り値を保存しておく

addObserverForName:を呼ぶと、引数で渡したブロックを持つオブザーバーのオブジェクトが返り値で返ってきます。addObserverForName:を使って登録したオブザーバーを解除するには、addObserverForName:の返り値で返されるオブジェクトをどこかで保持しておき、それをremoveObserver:の引数に与えてやる必要があります。

@property (weak) id myObserver;
---
- (void)viewDidAppear {
  self.myObserver = [[NSNotificationCenter defaultCenter]
    addObserverForName:UIApplicationDidEnterBackgroundNotification
    object:nil queue:[NSOperationQueue mainQueue]
    usingBlock:^(NSNotification *note) {
      //TODO:
    }
  ];
}
- (void)viewWillDisappear {
  [[NSNotificationCenter defaultCenter] removeObserver:self.myObserver];
}

複数のNotificationに対するオブザーブを同じタイミングで開始・解除したい場合は、どこかのクラスでNSSetとしてオブザーバーを保持するようにしておくと管理が楽です。(もちろん、解除のタイミングがそれぞれ別の場合は一緒にまとめちゃだめです)

@property (strong) NSMutableSet* observers;
----
- (void)viewDidAppear {
  self.observers = [NSMutableSet new];
  [self.observers addObject:[[NSNotificationCenter defaultCenter]
    addObserverForName:UIApplicationDidEnterBackgroundNotification
    object:nil queue:[NSOperationQueue mainQueue]
    usingBlock:^(NSNotification *note) {
      //TODO:
    }
  ]];
  [self.observers addObject:[[NSNotificationCenter defaultCenter]
    addObserverForName:UIApplicationWillEnterForegroundNotification
    object:nil queue:[NSOperationQueue mainQueue]
    usingBlock:^(NSNotification *note) {
      //TODO:
    }
  ]];
}
- (void)viewWillDisappear {
  for (id observer in self.observers)
    [[NSNotificationCenter defaultCenter] removeObserver:observer];
  self.observers = nil;
}

冷静に考えれば当然の内容ではありますが、addObserver:のパターンに慣れている状態で深く考えずにaddObserverForName:を使ってしまうとハマりがちな罠なので気をつけましょう。

Pocket

「addObserverForName:object:queue:usingBlock:の罠」への4件のフィードバック

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です