APNsを使ってRails(houston)からiOSアプリケーションにリモートPush通知を行う(Swift4.2)

iOSアプリケーションに通知機能を実装することになりました。複数のドキュメントを参照しながら実装を進める過程がそれなりに大変だったので、とりあえずPUSH通知が動く状態まで同じ環境であれば忘れたとしてもたどり着けるように残しておきたいと思います。

## iOSアプリケーションでPUSH通知機能がどのように実現されているか
iOSアプリケーションでは、APNsという仕組みを使ってサードパーティ製のアプリケーションからの通知をアップルの端末に転送しています。APNsについての詳細な解説は[LocalおよびPush Notificationプログラミングガイド：APNsの概要](https://developer.apple.com/jp/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/APNSOverview.html)をご覧ください。

## 環境
Ruby on Rails 5.1.2
Swift4.2
Xcode Version 10.1
iOS 12.1
iOS6 Plus、実機での動作確認
Apple Developer Programの登録

## リモートPUSH通知に必要な証明書を準備する
必要な証明書を揃えるための手順
1. CSRファイルの作成
Certificated Signing Request。認証局に対してSSLサーバー証明書への署名を申請する。作成していれば同じものを使う。初回作成のみ。
2. 開発用証明書(.cer)の作成
1.で作ったCSRファイルを参照します。作成した開発用証明書(.cer)は5.で作る予定のProvision Profileが参照します。そのため、通知機能を実装するために作成した開発用証明書 or 既存の開発用証明書が参照したCSRファイルと、Provision Profileが参照した開発用証明書が参照したCSRファイルは必ず合わせるようにしてください。既にある場合は作成する必要はありません。
3. AppIDの作成
初回作成でも後から変更できます。自分は他機能を実装後通知機能を実装しようとしたので、既存のAppIDの設定を変更しています。
4. 端末の登録
実機の登録が済んでいる場合は不要。
5. Provisioning Profileの作成
AppIDと2.で作った開発証明書を参照するので、自分のように後から通知機能を実装しようとした場合は設定を変更した後再作成する必要があります。また、1.で作ったCSRファイル
6. APNs用証明書の作成(.cer)
この証明書の作成でもCSRファイルが必要になります。作成後にダウンロードしてをダブルクリックすることで、キーチェインに登録します。この時点でAppIDを参照するとPush NotificationsのDevelopmentがEnabledになっているのが確認出来るかと思います。

<img width="587" alt="68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f32303139332f31346232393461612d636634392d343961622d306633382d3432393038623662613364392e706e67.png" src="https://qiita-image-store.s3.amazonaws.com/0/122643/a0e94628-a4a6-de60-c12c-629987398259.png">

登録後はキーチェインアプリでAppIDで検索すると証明書が確認できます。
7. APNs用証明書(.p12)の作成
APNs用の証明書から作成します。

自分はそれぞれ作成した証明書が参照していたCSRファイルが異なったことが原因でPUSH通知機能が動作しませんでした。その時は改めて全て最初から作り直して最終的には問題なく通知を行うことが出来ました。個人開発だとこの辺で困ることはないのかな、と思います。
各ファイルの作成方法、キーチェーンアクセスでの設定については、画像などを使って丁寧に解説されているドキュメントがありますのでそちらを参照してください。

>[プッシュ通知に必要な証明書の作り方2018 - Qiita](https://qiita.com/natsumo/items/d5cc1d0be427ca3af1cb)

## Xcode側で必要な設定

アプリの設定のCapabilitiesのPush NotificationをONに、CapabilitiesのBackground ModesをONにしてBackground fetchとRemote notificationにチェックを入れます。
![341a629f618dd34f993bc07f26c9355e.png](https://qiita-image-store.s3.amazonaws.com/0/122643/170c3d83-ff4e-1a0b-2e14-6fd9db883a1c.png)

![32970fa79495fb69286a3d8cd0ef685d.png](https://qiita-image-store.s3.amazonaws.com/0/122643/98fc9de2-35ed-50a3-5a4e-0c5049834ab3.png)


## iOSのコードの変更
AppDelegate.swiftのコードを編集していきます。
また、本来であれば各バージョンごとに通知の設定を用意しなければいけないのですが、問題を単純なものにするために環境に書いたバージョンのみをサポートするコードになっています。iOS10以下の設定の方法等は他のドキュメントを参照するようにしてください。

Swift側で実装する内容は以下になります。
- アプリ起動後にPush通知設定ダイアログを表示する
- Device Tokenを受信した時に呼び出されるメソッドの定義
- フォアグラウンドでの通知の受信を有効にするメソッドの定義
- APNsがPUSH通知を行った時に呼び出されるメソッドの定義

まずは起動後に通知設定ダイアログを表示する。開発用なのでわかりやすくするために通知設定が拒否されたらアプリを終了するよう実装してみます。その後起動しても通知の設定を見直して終了します。また、設定画面から通知設定をONにしたらアプリが起動できます。また、フォアグラウンドで通知を表示したり、通知に対するアクションをハンドリングするにはapplicationDidFinishLaunching(_:)でUNUserNotificationCentertDelegateを設定する必要があります。アプリ起動後に設定してもコールバックが呼ばれないようです。
>To guarantee that your app is able to respond to actionable notifications, you must set the delegate before your app finishes launching. For example, this means setting the delegate before an iOS app’s applicationDidFinishLaunching(_:) method returns.
>https://developer.apple.com/documentation/usernotifications/unusernotificationcenter/1649522-delegate


```swift:AppDelegate.swift
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
		// 通知設定の表示を行うメソッド
        registerForPushNotifications()
        return true
    }
		
	func registerForPushNotifications() {
        UNUserNotificationCenter.current()
            .requestAuthorization(options: [.badge, .sound, .alert], completionHandler: { (granted, error) in
                // 許可されない場合は、アプリを終了する
                if granted {
                    DispatchQueue.main.async {
                        UIApplication.shared.registerForRemoteNotifications()
                    }
                } else {
                    let alert = UIAlertController(
                        title: "エラー",
                        message: "プッシュ通知が拒否されています。設定から有効にしてください。",
                        preferredStyle: .alert
                    )
                    // 終了処理
                    let closeAction = UIAlertAction(title: "閉じる", style: .default) { _ in exit(1) }
                    alert.addAction(closeAction)
                    // ダイアログを表示
                    self.window?.rootViewController?.present(alert, animated: true, completion: nil)
                }
            })
    }
```

次はアプリの起動後実機でのPUSH通知のテストに必要なDevice Tokenを取得します。
Device Tokenを受信した時に呼ばれるメソッドを定義してprintで出力するよう実装します。合わせてリモート通知設定が失敗した時に呼ばれるメソッドを定義します。

```swift:AppDelegate.swift
    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        // Device Tokenを受信した時に呼ばれるメソッド
        let tokenParts = deviceToken.map { data -> String in
            return String(format: "%02.2hhx", data)
        }
        let token = tokenParts.joined()
        print("Device Token: \(token)")
    }
    
    func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
        // error if APNs の登録が失敗したときに呼ばれるメソッド
        print("Registration failed: \(error)")
    }
```

通知がうまく行かなかった時にiOSアプリ側の通知の設定に問題があるのか、通知の発送先に問題があるのか切り分けるためにローカルで何らかのアクションに紐づけてローカル通知を行ってみます。
通知の動作確認として適当なボタンなりを設置してアクションにフックして以下のコードが実行されるようにしてみてください。

```swift
        let content = UNMutableNotificationContent()
        content.title = "test"
        content.body = "test"
        // 通知音
        content.sound = UNNotificationSound.default
        // 通知を表示
        let request = UNNotificationRequest(identifier: "immediately", content: content, trigger: nil)
        UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
```
通知が表示された場合通知に関する設定が有効になっていると考えて良いと思います。

次にフォアグラウンドでの通知受信設定を行います。実装しないとフォアグラウンドで通知受信が出来ません。バックグラウンドのための実装は必要ありません。

```swift
  // フォアグラウンドでの通知受信。
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void)
    {
        print("フォアグラウンドで受信しました")
        completionHandler([ .badge, .sound, .alert ])
    }
```

最後にリモート通知を受け取った時に実行されるメソッドを定義します。

```swift
    func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
        switch application.applicationState {
        case .inactive:
            print("application.applicationState is inactive: \(userInfo)")
            
        case .active:
            print("application.applicationState is active: \(userInfo)")
        case .background:
            print("application.applicationState is background: \(userInfo)")
        }
    }
```

## リモート通信の送信
APNsの通知はにTLS通信で行います。必要な証明書および秘密鍵の生成を行います。APNs用証明書(.p12)を指定してpemファイルを作成します。

```sh
# APNs用証明書(.p12)にパスワードを指定している場合はpass:xxxxが必要
openssl pkcs12 -in APNs用証明書(.p12)のファイル名 -out 任意の名前.pem -nodes -clcerts
```

一通りiOS側での実装が終わったのでRails側の実装に入ります。Railsからのリモート通知送信機能を今回は[houston](https://github.com/nomad/houston)というgemを使って実装します。Installtionに従ってhoustonをインストールしてください。ローカルのPCからリモート通知がうまくいくか試したい(Ruby on Railsで構築したWebアプリケーションではなくRubyスクリプトで試したい)場合は`gem install houston`を実行してください。
動作確認はUsageにあるコードをそのまま流用します。

```ruby
# ローカルの場合のみrequire
require 'houston'

# Environment variables are automatically read, or can be overridden by any specified options. You can also
# conveniently use `Houston::Client.development` or `Houston::Client.production`.
APN = Houston::Client.development
# .pemファイルのパスを指定する。
APN.certificate = File.read('/path/to/apple_push_notification.pem')

# Device tokenを貼り付ける
token = '<token>'

# Create a notification that alerts a message to the user, plays a sound, and sets the badge on the app
notification = Houston::Notification.new(device: token)
notification.alert = 'Hello, World!'

# Notifications can also change the badge count, have a custom sound, have a category identifier, indicate available Newsstand content, or pass along arbitrary data.
notification.badge = 57
notification.sound = 'sosumi.aiff'
notification.category = 'INVITE_CATEGORY'
notification.content_available = true
notification.mutable_content = true
notification.custom_data = { foo: 'bar' }
notification.url_args = %w[boarding A998]

# And... sent! That's all it takes.
APN.push(notification)
```

このコードをrakeファイルで実行するか.rbファイルにこれをコピペして`ruby`コマンドの引数に渡せばリモート通知が送信されます。これで問題なく実機で通知を受け取れれば最低限の通知機能の実装が完了したことになります。なお、以下のコマンドを使って簡単にリモート通知を送信できます

```sh
apn push "<Device token>" -c apple_push_notification.pem -m "Hello from CLI"0
```

※追記
サンプルとしてRails側で実行したrakeファイルを載せます

```post_notification.rake
# Rakefile:

ROOT = File.expand_path(File.dirname(__FILE__))

task :push_notification do
  # Environment variables are automatically read, or can be overridden by any specified options. You can also
  # conveniently use `Houston::Client.development` or `Houston::Client.production`.
  APN = Houston::Client.development
  APN.certificate = File.read("config/pem/apple_push_notification_development.pem")

# An example of the token sent back when a device registers for notifications
  token = "token"

# Create a notification that alerts a message to the user, plays a sound, and sets the badge on the app
  notification = Houston::Notification.new(device: token)
  notification.alert = 'Hello, World!'

# Notifications can also change the badge count, have a custom sound, have a category identifier, indicate available Newsstand content, or pass along arbitrary data.
  notification.badge = 57
  notification.sound = 'sosumi.aiff'
  notification.category = 'INVITE_CATEGORY'
  notification.content_available = true
  notification.mutable_content = true
  notification.custom_data = { foo: 'bar' }
  notification.url_args = %w[boarding A998]

# And... sent! That's all it takes.
  APN.push(notification)



end
```



## application(_:didReceiveRemoteNotification:fetchCompletionHandler:)が呼ばれない
一通りの説明は終わりましたが、一点自分が解決に時間がかかったことがあるので追記します。自分はまず上記のapnコマンドを使って動作確認をしたのですが、そうするとapplication(_:didReceiveRemoteNotification:fetchCompletionHandler:)が呼ばれませんでした。これはリクエストボディの中に"content_available": trueが含まれていないことが原因のようです。
先述したRubyのコードを見ると`notification.content_available = true`と記載されています。そのため先述のRubyのコードを実行すればきちんとapplication(_:didReceiveRemoteNotification:fetchCompletionHandler:)
が呼ばれます。自分はこれに気付かず長時間時間を無駄にしました…。

## まとめ
以上で終わりになりますが、プログラミングの経験が浅いこともあり誤りや非効率な部分があるかと思います。何かお気づきの際はコメントにてご指摘お願いします。