Xcode で iOS アプリを開発する際の話ですが、プログラム内で通知をやり取りする場合、最近では protocol + delegate より、もっと便利でお手軽な NSNotificationCenter を使うことが多いのではないでしょうか。
さらに、addObserver する時も、より今っぽくブロックで処理を書きたいので、forName(addObserverForName)の方を使ったりすると思います。
すると、解除したつもりの登録が NotificationCenter に残ってしまい、意図せず同じ処理が複数走ってしまったり場合があるので、少しばかり気をつけましょうというお話です。
読み込み中です。少々お待ち下さい
登録が残ってしまう例
例えば、Xcode の新規プロジェクトで「Master-Detail Application」を選択し、「DetailViewController」の末尾に以下のようなコードを追加します。
ちなみに、例は Swift 3 ですが、Swift 2 でも Objective-C でも書き方が多少変わるだけで、根本は同じです。
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
NotificationCenter.default.addObserver(forName: DetailViewController.ForNameTestNotification, object: nil, queue: OperationQueue.main) { (notificaiton: Notification) in
print("forName test notification recieved.")
}
NotificationCenter.default.addObserver(self, selector: #selector(selectorTest(_:)), name: DetailViewController.SelectorTestNotification, object: nil)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
NotificationCenter.default.removeObserver(self)
}
func selectorTest(_ sender: AnyObject) {
print("selector test notification received.")
}
@IBAction func viewDidTap(_ sender: AnyObject) {
NotificationCenter.default.post(name: DetailViewController.ForNameTestNotification, object: nil)
NotificationCenter.default.post(name: DetailViewController.SelectorTestNotification, object: nil)
}
static let SelectorTestNotification = Notification.Name("SelectorTestNotification")
static let ForNameTestNotification = Notification.Name("ForNameTestNotification")
簡単なコードですので眺めるだけで一目瞭然ですが、いちおう補足しておくと、ストーリーボードで DetailView の方に TapGestureRecognizer を追加して View の gestureRecognizers と接続し、TapGestureRecognizer の Sent Actions をコードの viewDidTap と接続したら、準備は完了です。
アプリをビルドして、シミュレーターでも実機でもどこでもいいので起動し、MasterView が表示されたら右上の「+」ボタンをタップして一覧に日付を追加します。
その日付を選択して遷移した先の DetailView で任意の場所をタップすると、以下のようにコンソールに出力されます。
forName test notification recieved.
selector test notification received.
ここまでは、特に問題はありませんね。
さて、それでは一旦 MasterView に戻り、一覧の日付をもう一度タップして DetailView を開き直し、また画面をタップしてみましょう。
forName test notification recieved.
forName test notification recieved.
selector test notification received.
むむむ、「forName test notification recieved.」の出力が2回処理されてしまっていますね。
同じ操作を繰り返すと、以下のようにさらに増えていきます。
forName test notification recieved.
forName test notification recieved.
forName test notification recieved.
selector test notification received.
このままだと行ったり来たりする度に際限なく登録されてしまい、1回の通知で同じ処理が何度も走ってしまいますので、大変よろしくありません。
原因は
ところで、上の例では「selector test notification received.」の出力に関しては、常に1回しか処理されていません。
これは、追加した observer が viewDidDisappear の「NotificationCenter.default.removeObserver(self)」によって画面遷移のタイミングで削除されているからです。
それでは、なぜ forName で追加した方は削除されていないのでしょう。
少し注意して見るとすぐに分かるのですが、「NotificationCenter.default.addObserver(forName: ...」の方は、「addObserver(self, selector:...」の self にあたる observer が指定されていません。
ですので、「removeObserver(self)」としても、削除される道理がないという訳です。
このように、必要と思われる箇所で「removeObserver(self)」と書いておくと、なんとなく全ての登録を解除した気分になってしまいがちですが、実際はまるきり残っていたりしますので気をつけましょう。
対処方法
それでは、forName で登録した場合は、removeObserver の引数に何を指定すれば良いのでしょうか。
当たり前ですが、「NotificationCenter.default.removeObserver(self, name: DetailViewController.ForNameTestNotification...」 としても解除できません。
答えはスバリ、addObserver した時の戻り値です。
それを踏まえた上で、コードを修正してみましょう(赤字が追加部分です)。
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
observers.append(NotificationCenter.default.addObserver(forName: DetailViewController.ForNameTestNotification, object: nil, queue: OperationQueue.main) { (notificaiton: Notification) in
print("forName test notification recieved.")
})
NotificationCenter.default.addObserver(self, selector: #selector(selectorTest(_:)), name: DetailViewController.SelectorTestNotification, object: nil)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
NotificationCenter.default.removeObserver(self)
for observer in observers {
NotificationCenter.default.removeObserver(observer)
}
observers.removeAll()
}
@IBAction func viewDidTap(_ sender: AnyObject) {
NotificationCenter.default.post(name: DetailViewController.ForNameTestNotification, object: nil)
NotificationCenter.default.post(name: DetailViewController.SelectorTestNotification, object: nil)
}
func selectorTest(_ sender: AnyObject) {
print("selector test notification received")
}
var observers: [Any] = []
static let SelectorTestNotification = Notification.Name("SelectorTestNotification")
static let ForNameTestNotification = Notification.Name("ForNameTestNotification")
いちおう、複数登録に対応しています。
これなら、いくら行ったり来たりを繰り返しても、forName で登録した処理が意図せず何度も走ってしまうことはありません。
よく使うようであれば、サブクラス化してしまってもいいかも知れませんね。
おわりに
すごい簡単な話なのに、エラい長くなってしまった......
ちなみに、日本語表現として登録と追加、解除と削除が入り乱れており大変に申し訳ないのですが、検索対応ということで、どうぞご了承ください(いちおう、どちらかのセットにはなるように記述しています)。
また、コード以外の箇所で Swift 2、3、Objective-C の表記が一部混ざっていますが、これも意図的なものですので、どうぞご了承ください(Swift 3 で色々な名前が変わってしまったので)。