iOSにプッシュ通知を送るためのメモ-APNS

タイトルの通りです。やる度に記憶が失われるので今のうちに自分用に記録しておきます。

###やることリスト

- App IDの作成
- APNS用証明書の登録
- Provisioning Profileの作成
- p12生成 (pem生成も)
- Xcodeでの設定
- おくる！！！

###番外

- シミュレータでのプッシュ通知
- 開発用と本番用の違いは？TestFlightではどうなる？


まずはじめに絶対に注意すること
**開発用と本番用は混ぜちゃダメ**ってこと
動かなくなる原因なので気を付ける。

#1. App IDの作成

####(1)Apple Developer Programにログイン
[Apple Developer Program](https://developer.apple.com/account)

####(2)Certificates, Identifiers & Profiles をクリック

####(3)Identifiersのプラスボタンをクリック

・App IDs を選択してContinue
・App を選択してContinue
 
・Description と Bundle IDを入力
※Bundle IDはExplicitの方を選ぶこと。ワイルドカードではプッシュ通知は送れません。
![スクリーンショット](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258951/61249aed-f4b1-62b6-e2df-26079d6bb4aa.png)
・CapabilitiesのPush Notificationsにチェック
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258951/ee31700c-6ae4-3756-4c65-4d1cf0971c1e.png" width="300">
・登録する

これでAppIDはOK。
すでに作ってある場合は(3)のCapabilitiesを編集して保存するだけ。
リリース済みのアプリでも大丈夫。

#2. APNS用証明書の登録
####(1)キーチェーンアクセスを開いて以下の操作をする

- キーチェーンアクセス＞証明書アシスタント＞認証局に証明書を要求
- ユーザーのメールアドレスに開発者のアドレスを入力（DeveloperのAppleIDと同じでよい）
- 通称にわかるような名前をつける
- CAのメールアドレスは空でよい
- ディスクに保存にチェック
- 適当な場所にCertificateSigningRequest.certSigningRequestを作成
 - 場所も名前も自分がわかればなんでもよい

####(2)Apple Developer Programにログイン
[Apple Developer Program](https://developer.apple.com/account)

####(3)Certificates, Identifiers & Profiles をクリック

####(4)Certificatesのプラスボタンをクリック

- Apple Push Notification service SSL (Sandbox)を選択してContinue
 - ※本番用ならその下のSandbox & Production

![スクリーンショット 2021-05-18 20.10.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258951/5ca7c8b5-dce6-9f3b-91b4-e40f28ff7dcb.png)

- Pratform: iOS
- 1で作ったApp IDを選択する
- 作成したらDownloadする
- Downloadしたファイルをダブルクリック(キーチェーンアクセスに登録される)

キーチェーンアクセスでこうなっていればOK
![スクリーンショット 2021-05-18 20.16.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258951/e4e34ce1-dca2-0b22-7ed6-4f63ff31619f.png)


#3. Provisioning Profileの作成

これはいつも通りです。
####(1)Apple Developer Programにログイン
[Apple Developer Program](https://developer.apple.com/account)

####(2)Certificates, Identifiers & Profiles をクリック

####(3)Profilesのプラスボタンをクリック

- iOS App Developmentを選択してContinue
 - ※Ad HocならAd Hoc、ストア申請用ならApp Store
- 1で作ったApp IDを選択してContinue
- 必要な開発者用証明書をチェックしてContinue
- 使いたいデバイスを選択してContinue
- Provisioning Profile Nameを入力してGenerate
- 作成したらDownloadする
- Downloadしたファイルをダブルクリック（Xcodeに登録される）

#4. p12生成 (pemも)

p12とpemどっちを使うはサーバー側の環境に合わせてください。
PHPから送信した時はpemを使って、javaからのときはp12を使いました。

####(1)キーチェーンアクセスを開く

####(2)2でダウンロードして登録したcerファイルを選択
開発用ならApple Development IOS Push Services: バンドルID
本番用ならApple Push Service: バンドルID
![スクリーンショット 2021-05-18 20.16.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258951/26e8ce7c-b00b-55a9-38a1-efc6b27e1cd2.png)

####(3)ファイルを書き出す
**ここからが罠**
本当に引っ掛かった。証明書だけを書き出すとか二つまとめて書き出すとか、そこ違うんだ？という感じ。
これが合っていないせいで「DeviceTokenNotForTopic」のエラーをしぬほど見た。
ちなみに証明書を別々に書き出してから合体させる方法もあるみたいですが、割愛します。

####★p12ファイルを使う場合

![スクリーンショット 2021-05-18 20.38.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258951/c0504485-97b9-d73c-7253-3187ec6ebde0.png)
・左側の三角を押して開き、秘密鍵を表示している状態で**上の証明書だけ**を選択して右クリック
![スクリーンショット 2021-05-18 20.38.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258951/839c708b-22c6-b906-559c-a9414da4868a.png)
・"Apple Development IOS Push Service: バンドルID"を書き出すを選択
・p12形式で任意の名前で書き出す。
・パスワードを設定できる。設定しないで空にしてもよい

####★pemファイルを使う場合

![スクリーンショット 2021-05-18 21.48.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258951/2283a178-f4f3-e1b7-0b4b-a3046d472bd9.png)
・左側の三角を押して開き、**証明書と鍵の二つを選択した状態**で右クリック
![スクリーンショット 2021-05-18 20.38.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258951/e9387a8d-6362-c555-0c36-df7f493148c0.png)
・2項目を書き出すを選択
・p12形式で任意の名前で書き出す。
・パスワードを設定できる。設定しないで空にしてもよい
>
$ cd p12ファイルを書き出した場所
$ openssl pkcs12 -in pushCert.p12 -out pushCert.pem -nodes -clcerts

・ターミナルで操作しpemファイルを書き出す
パスワードを設定していたらここで聞かれる

#5. Xcodeでの設定
####(1)TARGET＞Signing & Capabilitiesを選択
+Capabilityをクリックし、Push Notificationsを追加

####(2)デバイストークン取得処理を書く

```swift:AppDelegate.swift
import UserNotifications

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
}
```

```swift:AppDelegate.swift
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    
    if launchOptions != nil {
      if let _ = launchOptions?[UIApplication.LaunchOptionsKey.remoteNotification] as? Dictionary<String, Any?> {
        // 未起動時に通知から起動したとき。
      }
    }
    return true
  }
```

```swift:AppDelegate.swift
  // MARK: - Push Notifications
  // Remote Notification の device token を表示
  func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    let tokenParts = deviceToken.map { data -> String in
      return String(format: "%.2hhx", data)
    }
    let token = tokenParts.joined()
    print("Device Token: \(token)")
  }
  
  func registerForPushNotifications() {
    UNUserNotificationCenter.current().delegate = self
    UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) {
      (granted, error) in
      guard granted else { return }
      DispatchQueue.main.async {
        UIApplication.shared.registerForRemoteNotifications()
      }
    }
  }
  
  func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
    switch (application.applicationState) {
    case .active, .inactive:
      // 起動しているorバックグラウンド起動時に通知をタップしたとき。
    case .background:
      break
    default:
      break
    }
  }
  
  func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
    completionHandler([.alert])  // 通知バナー表示
  }
```

#6. おくる！

####①ツールで送ってみる
今回使ったのはこちら。Pusherっていうのを前は使っていたんだけど、なぜか送れなかったので乗り換えました。こっちはp12を使用。
[PushNotificationsTester](https://github.com/onmyway133/PushNotifications)
Androidにも送れるらしい。
![スクリーンショット 2021-05-18 22.00.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/258951/c95d1da2-836f-cf0f-3cf4-532e22a36b4b.png)
起動したら、1で設定したバンドルID、4で生成したp12ファイル、5で取得したデバイストークンを設定して送信する。

####②curlで送ってみる
コマンドからも簡単に送れる。こっちはpemを使用。

>
$ curl -v -d '{"aps":{"alert":"message"}}' -H "apns-topic:バンドルID" --http2 --cert appCert.pem https://api.development.push.apple.com/3/device/デバイスID


#番外

####シミュレータでのプッシュ通知

シミュレータでもプッシュ通知を受け取ることができます。
これは証明書などを使わないので、アプリ側の通知受取時の動きを確認したい時に便利。

####(1)JSONファイルを用意する

```json:test.apns
{
  "Simulator Target Bundle": "バンドルID",
  "aps":{
      "alert":"message",
      "sound":"default",
      "badge":1
  }
}
```
JSON形式だけど拡張子は「.apns」で保存。受け取りたい通知の内容を記載してください。

####(2)アプリをインストール済みのシミュレータの画面上にファイルをドラッグ&ドロップ
これだけです！簡単◎
通知を受け取ったら自動的にお知らせ画面に遷移させたいなーとか、そういう実装の確認をする時にいちいち端末にプッシュ通知を送るのは面倒なのでこの方法で確認します。

####開発用と本番用の違いは？TestFlightではどうなる？

・デバイストークンが異なる。おなじバンドルIDだったとしても取れるトークンは違うので分ける必要がある。
・一気に複数のデバイストークンに対してループなどで送っている時に開発用と本番用が混ざっていると、以降の通知が飛ばなくなる。サーバー上で混在してしまわない様に保存先はきちんと分けないとだめ。
・TestFlightでは本番用が使われる。テストだからって開発サーバーに向けていると大変なことになるので注意。

書き切れないほどたくさんの記事やサイトを参考にしています。
まとめてくださってる方々に感謝です…！ありがとうございます。