目的

iOSアプリでpush通知を設定するためのメモ

環境

  • Xcode Version 12.4
  • Swift 5

参照サイト

Implementing Alert Push Notifications
Asking Permission to Use Notifications
UNAuthorizationOptions
Sending Push Notifications Using Command-Line Tools UNNotificationPresentationOptions

対応メモ

Xcode作業とDeveloperサイトで証明書を作成・取得が必要です。
ページ最後には実際PCからiOS端末にpush通知をテストしています。

Xcodeでの作業

Xcodeでのプログラムの実装

まずXcodeでのプログラムの実装です。
プログラムはAppDelegateクラスにextensionとしてこれ以降をすべて追記していく。

AppDelegateにextensionとして記載
extension AppDelegate: UNUserNotificationCenterDelegate {
    ....
    //↓の項目のプログラムをここに追記していく
}

次に利用者にプッシュ通知を許可するかどうかを申請するコード
3行目の .provisional はコメント化している。
基本は.alert, .sound, .badgeだけで良いのだが、
.provisionalについては、iOS12から導入された機能で、ユーザが暫定的にpush通知を受け取る機能。
ちなみに2021/4/7時点でここのUNAuthorizationOptionsのalertはdeprecatedではない。
後述のUNNotificationPresentationOptionsではalertはdeprecatedとなっている。

端末にプッシュ通知の許可を申請するコード
    func registerForPushNotifications() {
        let center = UNUserNotificationCenter.current()
        center.requestAuthorization(options: [.alert, .sound, .badge/*, .provisional*/]) {granted, error in//.provisionalは環境によって設定
            if error == nil {
                center.getNotificationSettings { settings in
                    guard (settings.authorizationStatus == .authorized) else { return }

                    DispatchQueue.main.async {
                        UIApplication.shared.registerForRemoteNotifications()
                    }
                }
            }
        }
    }

次にAppleサーバからトークンが配布された時に自動で呼び出されるコード
取得したらpush通知サービスにトークンの登録が必要
取得時に画面表示するサンプルも一応載せておくが、あんまりやらないかな。

トークン取得が成功した時に呼ばれる
    //トークン取得が成功した時に呼ばれる
    func application(_ application: UIApplication,
                     didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        let tokenComponents = deviceToken.map { data in String(format: "%02.2hhx", data) }
        let deviceTokenString = tokenComponents.joined()

        //トークン取得時にサーバとかにトークンを登録する
        self.forwardTokenToServer(tokenString: deviceTokenString)

        /*
        トークン取得時に画面表示するサンプル
        guard let viewController = UIApplication.shared.windows.first!.rootViewController as? ViewController else {
            return
        }
        viewController.showDeviceToken(deviceTokenString)
        */
    }

トークン取得に失敗した時のコード

トークン取得が失敗した時に呼ばれる
    func application(_ application: UIApplication,
                     didFailToRegisterForRemoteNotificationsWithError error: Error) {
        print("error: \(error.localizedDescription)")

        /*
        トークン取得失敗時に画面表示するサンプル
        guard let viewController = UIApplication.shared.windows.first!.rootViewController as? ViewController else {
            return
        }

        //viewController.showDeviceToken(error.localizedDescription)
        */
    }

push通知をタップした時に呼ばれるコード
ここでpush通知に付随しているデータを取得できる。
また、バッジを設定している場合は、数字アイコンを消す設定も必要。

push通知をタップした時に呼ばれる
    func userNotificationCenter(_ center: UNUserNotificationCenter,
                                didReceive response: UNNotificationResponse,
                                withCompletionHandler completionHandler: @escaping () -> Void) {
        //push通知に付随しているデータを取得
        let userInfo = response.notification.request.content.userInfo
        
        //通知を押したので通知フラグのアイコンを消す
        UIApplication.shared.applicationIconBadgeNumber = 0
        
        
        completionHandler()
        
        /*
        guard let viewController = UIApplication.shared.windows.first!.rootViewController as? ViewController else {
            return
        }

        //画面表示のサンプル
        viewController.show(userInfo)
        */
    }

アプリがフォアグラウンドの時に通知が来た際に呼ばれるコード
completionHandlerはpush通知の表示設定
alertはiOS14からdeprecatedになり、代わりにlistbannerとなった。
listは通知センターに表示させる仕様。bannerはホーム画面上部に表示させる仕様。
list + banner = alertと同等の機能だった。

ただ、アプリ起動中にそのアプリのpush通知を表示するのはユーザビリティ的に良くない事を聞く。
さらにbadgeを設定すると残り続けるので(その通知をタップした場合はuserNotificationCenter~didReceiveで消せるけど)
applicationIconBadgeNumberで消す必要がある。
badge自体設定しないほうがよいかも。

アプリがフォアグラウンドの時に通知がくる際に呼ばれる
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        
        //push通知設定したい場合
        //badgeは設定しないほうが良いかも
        completionHandler([.list, .banner, .badge, .sound])//alertはdeprecated
        
        //通知を押したので通知フラグのアイコンを消す
        //上部でbadgeを設定した場合、消せる
        UIApplication.shared.applicationIconBadgeNumber = 0

    }

トークンをサーバに登録するときのサンプルコード
以下ではURLComponents使ったサンプルです。
AlamofireとかGoogleの場合は別途ドキュメントを参照ください。

サーバにトークンを送信する場合の処理サンプル
    func forwardTokenToServer(tokenString: String) {
        print("Token: \(tokenString)")
        
        /*
        let queryItems = [URLQueryItem(name: "deviceToken", value: tokenString)]
        var urlComps = URLComponents(string: "www.example.com")!
        urlComps.queryItems = queryItems
        guard let url = urlComps.url else {
            return
        }

        let task = URLSession.shared.dataTask(with: url) { (data: Data?, response: URLResponse?, error: Error?) in
            if error != nil {
                // Handle the error
                return
            }
            guard response != nil else {
                // Handle empty response
                return
            }
            guard data != nil else {
                // Handle empty data
                return
            }

            // Handle data
        }

        task.resume()
        */
    }

後は上記registerForPushNotificationsコードを呼ぶようにします。
以下はアプリ起動時AppDlegateのdidFinishLaunchingWithOptionsに呼ぶときのサンプルです。

ただ、業務用アプリとかでしたらこのタイミングで良いのですが、公開アプリですと、起動時の初回で呼ぶのは良くないと言われていますよね。
初回でいきなりpush通知の許可ポップアップが出てしまうのですが、そのタイミングで許可するユーザは多くないのでは
ということが結構いわれています。
ある程度アプリに慣れてきたタイミングでpush通知の許可を求めるべき等。。。

トークン設定
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        
        registerForPushNotifications()//追加
        
        return true
    }

Xcodeでファイル設定

次にXcodeでpush通知用のファイル設定を行う。
設定を行わないと、トークンが発行されない。

またTARGETSSigning & Capabilityより + Capabilityをクリック。

Push Notificationsをクリック

設定されていることを確認

アプリを実行

これでアプリを実行してみると、push通知のポップアップが表示され、
ログ出力画面にトークンが表示されると思う。

Token: aaaaxxxxx21853a2848cc18514e88888888yyyyyyyyyyyyyyyyyyyyxxxxxxxxxx

Developerサイトでの作業

次にDeveloperサイトにてpush通知証明書を作成・取得する。
Apple Developer

Certificates, Identifiers & Profilesページより
左ペインよりKeysをクリックして、Create a keyを選択

key名を適切に入力後、Apple Push Notifications service (APNs)を選択し、Continueボタンをクリック。

設定値を確認して、次へ

Downloadボタンをクリックして証明書をダウンロード
証明書はこのタイミングでしか取得できないので、超注意!

ダウンロードした証明書はサーバやサービスに配置する。

push通知実施

ということで、実際にpush通知のテストを行ってみる。
テストということでMacからpush通知を投げて実機で受け取ってみる。

まずはシェルファイルを作成する。内容は以下。
apns.shなどで保存する。

  • TEAM_IDはApple DeveloperのチームID。Developerサイトやら、Xcode上でプロジェクト検索でTEAMで検索しても表示され、確認できる。
  • TOKEN_KEY_FILE_NAMEはp8形式のpush証明書のこと。
  • AUTH_KEY_IDは証明書のKey ID。push証明書のファイル名と同じである(と思われる)
  • TOPICはアプリのBundle Identifier
  • DEVICE_TOKENは今回取得したデバイストークン(上記項目参照)
  • APNS_HOST_NAMEはAppleサーバのホスト名。テストの場合はこの値固定となる
apns.sh
#!/bin/bash

TEAM_ID=XXXXXYYYYY
TOKEN_KEY_FILE_NAME="./AuthKey_ZZZZZZZZZZ.p8"
AUTH_KEY_ID=ZZZZZZZZZZ
TOPIC=com.DDDDD.pushnotification
DEVICE_TOKEN=aaaaxxxxx21853a2848cc18514e88888888yyyyyyyyyyyyyyyyyyyyxxxxxxxxxx
APNS_HOST_NAME=api.sandbox.push.apple.com

JWT_ISSUE_TIME=$(date +%s)
JWT_HEADER=$(printf '{ "alg": "ES256", "kid": "%s" }' "${AUTH_KEY_ID}" | openssl base64 -e -A | tr -- '+/' '-_' | tr -d =)
JWT_CLAIMS=$(printf '{ "iss": "%s", "iat": %d }' "${TEAM_ID}" "${JWT_ISSUE_TIME}" | openssl base64 -e -A | tr -- '+/' '-_' | tr -d =)
JWT_HEADER_CLAIMS="${JWT_HEADER}.${JWT_CLAIMS}"
JWT_SIGNED_HEADER_CLAIMS=$(printf "${JWT_HEADER_CLAIMS}" | openssl dgst -binary -sha256 -sign "${TOKEN_KEY_FILE_NAME}" | openssl base64 -e -A | tr -- '+/' '-_' | tr -d =)
AUTHENTICATION_TOKEN="${JWT_HEADER}.${JWT_CLAIMS}.${JWT_SIGNED_HEADER_CLAIMS}"

curl -v --header "apns-topic: $TOPIC" --header "apns-push-type: alert" --header "authorization: bearer $AUTHENTICATION_TOKEN" --data '{"aps": {"badge": 1,"alert": {"title": "title","subtitle": "subtitle","body": "bodystring"},"content-available": 1}}' --http2 https://${APNS_HOST_NAME}/3/device/${DEVICE_TOKEN}

上記シェルと同階層にpush通知証明書を設置する。

ターミナルを開き、上記フォルダに移動。

その後、以下のコマンドを実施して、テストpush通知を実行する。
※うまく設定できれいれば、トークンを取得したiOS端末にのみpush通知が届きます。

push通知実施
$ sh ./apns.sh  

↓
ズラーとログが表示され、
問題ない場合は
* Closing connection 0
と表示。問題がある場合は、
{"reason":"MissingProviderToken"}* Closing connection 0
と表示される。

問題ない場合は端末にpush通知が届く
ホーム画面での表示
(使っているiPhoneの設定で、ホーム画面でのプレビュー表示はしないにしている)

通知センターでの表示

以上です。