[Xcode/Swift] NotificationCenterの基本を知る

1. はじめに

NotificationCenter とは?

iOS アプリを開発していると、こんな場面に出会うことがよくあるかと思われます。

  • ログインが完了したら、別の画面に「ログインした」と伝えたい
  • 設定画面でテーマを変えたら、全画面に反映したい
  • どこかで起きたイベントを、直接つながっていない複数のクラスに知らせたい

こういったときクラス同士を直接つなぐと、コードが複雑に絡み合って保守性の高いコードは書けなくなります。

そこで活躍するのが NotificationCenter です。

NotificationCenter は一言で言うと 「アプリ内放送局」 です。

  1. 誰かが「ニュース速報」と送信
  2. 放送局が受信して全員に流す
  3. 聞いていた人(Observer)全員に届く

クラス同士が直接やり取りするのではなく、NotificationCenter を介することで、送り手と受け手を 疎結合(お互いを知らなくてよい状態)に保てます。

どんな場面で使うのか

NotificationCenter が特に役立つのは、次のような状況です。

場面具体例
1対多の通知ログイン完了を複数画面に一斉通知
画面をまたいだ通知設定画面の変更をトップ画面に伝える
システムイベントの検知キーボードの表示・非表示、アプリのバックグラウンド復帰

逆に、1対1のシンプルなやり取りなら delegateclosure の方がすっきり書けることも多いので、使い分けはその都度考えるのがGoodです。

EnvironmentObject じゃダメなの?

SwiftUI を使っているなら「EnvironmentObjectで状態を共有すればよくない?」と思った方もいるかもしれません。確かに、View 間でデータを共有するだけならEnvironmentObjectの方がシンプルでスッキリ書けます。

ただし、NotificationCenter には `EnvironmentObject` にはない強みがあります。

比較EnvironmentObjectNotificationCenter
主な用途View 間の状態共有イベントの通知・伝達
SwiftUI 依存あり(View 階層が必要)なし(UIKit・SwiftUI 両対応)
システムイベントの検知❌ できない✅ できる
View 階層の外への通知❌ 届かない✅ 届く
タイミングの通知(一発イベント)苦手✅ 得意

特に次のケースでは NotificationCenter に軍配が上がります。

  • UIKit と SwiftUI が混在しているプロジェクト
  • キーボード表示・アプリのバックグラウンド復帰などシステムイベントを検知したい場面
  • View 階層に乗っていない Model や Service 層からイベントを飛ばしたい場面

「状態を持ち回る」なら EnvironmentObject、「イベントを知らせる」なら NotificationCenter と覚えておくと使い分けの判断がしやすくなります。

この記事で作るもの

ボタンを押すと通知が飛び、別のクラスがそれを受け取ってコンソールに表示する、というシンプルな仕組みを作って理解できるようにします。

[ Sender クラス ] –「通知を送信」–> [ NotificationCenter ]
|
v
[ Receiver クラス ]
「通知を受け取ったよ」と出力


2. 基本の仕組みを理解する

3つの登場人物

NotificationCenter を理解するには、3つの概念を押さえるだけで大丈夫です。

① Notification(通知)

やり取りされるメッセージ本体です。「名前」を持っており、その名前で通知を識別します。値(データ)を一緒に乗せることもできます。

// 通知の「名前」を定義する
// Notification.Name を extension で定義しておくと、文字列のタイポを防げて安全
extension Notification.Name {
    static let didUserLogin = Notification.Name("didUserLogin")
}

② Post(送信側)

「通知を送る」役割です。NotificationCenter に向けて post を呼ぶだけでよく、誰が受け取るかは気にしません。

NotificationCenter.default.post(name: .didUserLogin, object: nil)

③ Observer(受信側)

「通知を受け取る」役割です。あらかじめ addObserver で「この名前の通知が来たら教えて」と登録しておきます。

NotificationCenter.default.addObserver(
    self,
    selector: #selector(handleLogin),
    name: .didUserLogin,
    object: nil
)


### 通知の流れ
┌─────────────┐        post()        ┌──────────────────────┐
│   Sender    │ ──────────────────▶  │   NotificationCenter  │
│  (送り手)  │                      │     (放送局)         │
└─────────────┘                      └──────────┬───────────┘
                                                 │ 登録済みの
                                                 │ Observer に配信
                                    ┌────────────▼────────────┐
                                    │       Receiver           │
                                    │      (受け手)           │
                                    │  handleLogin() が呼ばれる │
                                    └─────────────────────────┘

ポイントは Sender と Receiver がお互いを知らない ことです。放送局を挟むことで、どちらかを変更しても影響が出ません。


3. 【ハンズオン①】まず動かしてみる

実際に手を動かして理解してみましょう。

Step 1|通知の名前を定義する

まず、やり取りする通知に名前をつけます。

import Foundation

extension Notification.Name {
    static let didTapButton = Notification.Name("didTapButton")
}

Step 2|Receiver を作る(先に登録する)

通知を受け取るクラスを作り、addObserver で登録します。

送信より先に登録しないと通知を受け取れないので注意してください。

class Receiver {
    init() {
        // 通知の受信を登録する
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(handleTap),
            name: .didTapButton,
            object: nil
        )
        print("Receiver: 登録完了、通知を待っています")
    }

    @objc func handleTap(_ notification: Notification) {
        print("Receiver: 通知を受け取りました!")
    }

    deinit {
        NotificationCenter.default.removeObserver(self)
        print("Receiver: 登録解除しました")
    }
}

@objcselector で指定するために必要なアノテーションです。deinit での removeObserver については、後のセクションで詳しく説明します。

Step 3|Sender を作る

通知を送るクラスです。post を呼ぶだけのシンプルな構造です。

class Sender {
    func tapButton() {
        print("Sender: ボタンが押されました、通知を送ります")
        NotificationCenter.default.post(name: .didTapButton, object: nil)
    }
}

Step 4|動かしてみる

// Playgroundなら
let receiver = Receiver()
let sender = Sender()

sender.tapButton()

**出力結果:**
Receiver: 登録完了、通知を待っています
Sender: ボタンが押されました、通知を送ります
Receiver: 通知を受け取りました!

// SwiftUIでざっくり動かしたいなら
import SwiftUI

final class Receiver {
    init() {
        // 通知の受信を登録する
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(handleTap),
            name: .didTapButton,
            object: nil
        )
        print("Receiver: 登録完了、通知を待っています")
    }
    
    @objc func handleTap(_ notification: Notification) {
        print("Receiver: 通知を受け取りました!")
    }
    
    deinit {
        NotificationCenter.default.removeObserver(self)
        print("Receiver: 登録解除しました")
    }
}

final class Sender {
    func tapButton() {
        print("Sender: ボタンが押されました、通知を送ります")
        NotificationCenter.default.post(name: .didTapButton, object: nil)
    }
}

struct ContentView: View {
    private let receiver = Receiver()
    private let sender = Sender()
    
    var body: some View {
        Button {
            sender.tapButton()
        } label: {
            Text("通知を送る")
        }
    }
    
}

extension Notification.Name {
    static let didTapButton = Notification.Name("didTapButton")
}

4. 【ハンズオン②】値(userInfo)を一緒に送る

セクション3では「通知を送る・受け取る」基本形を学びました。しかし実際のアプリでは、通知と一緒にデータも渡したい場面がよくあります。

例えば:

  • ログイン通知と一緒に「ユーザー名」を渡したい
  • ダウンロード完了通知と一緒に「進捗率」を渡したい

そこで使うのが userInfo です。

userInfo とは?

userInfo は通知に付け乗せできる 辞書(Dictionary) です。型は [AnyHashable: Any]? で、どんな値でも入れられます。

[ Sender ]  --「通知 + {"username": "Taro"}」-->  [ NotificationCenter ]  -->  [ Receiver ]

Step 1|通知名を定義する

先ほどと同様に、通知名を定義します。

import Foundation

extension Notification.Name {
    static let didUserLogin = Notification.Name("didUserLogin")
}

Step 2|userInfo に値を乗せて送信する

postuserInfo 引数に Dictionary を渡します。

class Sender {
    func login(username: String) {
        print("Sender: ログイン通知を送ります(ユーザー: \(username))")

        NotificationCenter.default.post(
            name: .didUserLogin,
            object: nil,
            userInfo: ["username": username]  // ここで値を乗せる
        )
    }
}

Step 3|受け取り側で値を取り出す

NotificationuserInfo プロパティから値を取り出します。Dictionary なので、キーを指定してアクセスします。

class Receiver {
    init() {
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(handleLogin(_:)),  // 引数ありの形にする
            name: .didUserLogin,
            object: nil
        )
    }

    @objc func handleLogin(_ notification: Notification) {
        // userInfo から値を取り出す
        guard let userInfo = notification.userInfo,
              let username = userInfo["username"] as? String else {
            print("Receiver: userInfo が取得できませんでした")
            return
        }

        print("Receiver: \(username) さんがログインしました!")
    }

    deinit {
        NotificationCenter.default.removeObserver(self)
    }
}

// 動作確認例
let receiver = Receiver()
let sender = Sender()

sender.login(username: "Taro")
sender.login(username: "Hanako")

**出力結果:**
Sender: ログイン通知を送ります(ユーザー: Taro)
Receiver: Taro さんがログインしました!
Sender: ログイン通知を送ります(ユーザー: Hanako)
Receiver: Hanako さんがログインしました!

userInfo["username"] の型は Any? なので、as? String型キャストして使います。guard let で安全に取り出すのが定石です。

複数の値を乗せたい場合

Dictionary なので、複数のキーを入れることもできます。

NotificationCenter.default.post(
    name: .didUserLogin,
    object: nil,
    userInfo: [
        "username": "Taro",
        "userId": 123,
        "isPremium": true
    ]
)

// 受け取り側も同様に、キーごとに取り出す
@objc func handleLogin(_ notification: Notification) {
    guard let userInfo = notification.userInfo,
          let username = userInfo["username"] as? String,
          let userId = userInfo["userId"] as? Int else { return }

    print("Receiver: \(userId) - \(username) さんがログインしました!")
}

文字列キーをそのまま書くと、タイポによるバグが起きやすいので定数として定義しておくのがおすすめです。

enum NotificationUserInfoKey {
    static let username = "username"
    static let userId = "userId"
}

// 送信側
userInfo: [NotificationUserInfoKey.username: "Taro"]

// 受信側
let username = userInfo[NotificationUserInfoKey.username] as? String

5. ⚠️ メモリリークに注意!

NotificationCenter は便利な反面、使い方を誤るとメモリリークを引き起こします。初心者がはまりやすいポイントなので、しっかり押さえておきましょう。

なぜメモリリークが起きるのか?

addObserver で登録すると、NotificationCenter は Observer への参照を保持します。

NotificationCenter.default
        │
        └──参照を保持──▶ Receiver インスタンス

ここで問題になるのが、Receiver が画面から消えた(= 不要になった)のに、NotificationCenter がまだ参照を持ち続けている状態です。

❌ 悪い状態

Receiver を使い終わって破棄したつもり
        │
        └── でも NotificationCenter がまだ参照中
                → メモリが解放されない
                → handleLogin が何度も呼ばれ続ける(ゾンビ状態)

特に、画面を何度も開いたり閉じたりするアプリでは、Observer がどんどん増え続ける

解決策:deinit で removeObserver する

Observer が不要になるタイミング(= インスタンスが破棄されるとき)に、登録を解除します。Swift では deinit がそのタイミングです。

class Receiver {
    init() {
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(handleLogin(_:)),
            name: .didUserLogin,
            object: nil
        )
    }

    @objc func handleLogin(_ notification: Notification) {
        print("ログイン通知を受け取りました")
    }

    // ✅ インスタンス破棄時に必ず解除する
    deinit {
        NotificationCenter.default.removeObserver(self)
    }
}

deinit に書いておけば、インスタンスが破棄されるときに自動で呼ばれるので、書き忘れを防ぎやすくなります。

block-based API を使う場合は特に注意

addObserver にはもう一つの書き方として、block-based API があります。

class Receiver {
    // トークンを保持する変数
    var token: NSObjectProtocol?

    init() {
        token = NotificationCenter.default.addObserver(
            forName: .didUserLogin,
            object: nil,
            queue: .main
        ) { [weak self] notification in   // ← [weak self] を忘れずに!
            guard let self = self else { return }
            self.handleLogin(notification)
        }
    }

    func handleLogin(_ notification: Notification) {
        print("ログイン通知を受け取りました")
    }

    // ✅ token を使って解除する
    deinit {
        if let token = token {
            NotificationCenter.default.removeObserver(token)
        }
    }
}
```



```
❌ 循環参照の図

Receiver ──strong──▶ token(block)
    ▲                      │
    └────strong────────────┘
         deinit が呼ばれない!
```
```
✅ [weak self] を使った場合

Receiver ──strong──▶ token(block)
    ▲                      │
    └─────weak─────────────┘
         Receiver が不要になれば deinit が呼ばれる

block-based API 2つの注意点

[weak self] を忘れると循環参照になる

block の中で self を強参照すると、Receiverblockself(Receiver) という循環参照が生まれ、deinit が呼ばれなくなります。必ず [weak self] を指定しましょう。

❌ 循環参照の図

Receiver ──strong──▶ token(block)
    ▲                      │
    └────strong────────────┘
         deinit が呼ばれない!

✅ [weak self] を使った場合

Receiver ──strong──▶ token(block)
    ▲                      │
    └─────weak─────────────┘
         Receiver が不要になれば deinit が呼ばれる

removeObserver には token を渡す

selector 形式と違い、block-based API では removeObserver(self) ではなく、addObserver戻り値(token)を渡す必要があります。token を var で保持しておくのはそのためです。


7. まとめ & 次のステップ

学んだことの振り返り

この記事では、NotificationCenter の基本から実践的な使い方まで学びました。

セクション学んだこと
基本の仕組みPost / Observer / Notification の3つの役割
ハンズオン①addObserverpost で通知を送受信する
ハンズオン②userInfo でデータを一緒に渡す
メモリリークdeinitremoveObserver して安全に解除する

NotificationCenter を使うべき場面・避けるべき場面

NotificationCenter は便利ですが、何でも使えばいいわけではありません。場面に応じて使い分けるのが大切です。

状況おすすめの手段
1対多で通知したい✅ NotificationCenter
システムイベントを検知したい✅ NotificationCenter
1対1のシンプルなやり取りdelegateclosure
データの流れを宣言的に書きたいCombine
Swift Concurrency を使っているAsyncStreamactor

NotificationCenter は送り手と受け手が疎結合になるのが強みですが、その分「どこから通知が来るのか」が追いにくくなる弱点もあります。小規模なやり取りには delegateclosure を使い、NotificationCenter は本当に必要な場面に絞るのがベストプラクティスです。