【Swift】iOSアプリにプッシュ通知を実装したサンプルアプリを作ってみた

__2023/01、本記事を完全リニューアルしました:dizzy:__
__添付のサンプルアプリも更新しています:ok_hand:__
__2023/02、ペイロード取得サンプルも更新完了しました:dizzy:__

# はじめに

「Swiftで作ったiOSアプリにプッシュ通知の実装をしたい！」という方へにはこの記事が参考になるかと思います:thumbsup:
手順に従って設定を行えばすぐに動作確認ができるサンプルアプリを用意していますので、ご自由にご利用ください。
プッシュ通知ASPとして ニフクラ mobile backend を利用しています。

iOSのプッシュ通知はこんな感じで受信されます↓

<img alt="iOS_push.jpeg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/112032/e8c93d87-7098-cc73-c4aa-36d6b5b3c060.jpeg" width="300">

## ニフクラ mobile backend　とは

https://mbaas.nifcloud.com/

スマートフォンアプリのバックエンド機能（プッシュ通知・データストア・会員管理・ファイルストア・SNS連携・位置情報検索・スクリプト）が**開発不要**、**無料**(注1)から使えるクラウドサービスです。

注1：詳しくは[こちら](https://mbaas.nifcloud.com/price.htm)をご覧ください


# 動作環境

プロジェクトに組み込んでいるニフクラ mobile backend の Swift　SDKのバージョンはドキュメント更新時点で最新の __v1.3.1__ です。このバージョンでの動作環境は下記です。

* Swift version 4.2, 5.x
* iOS 13.x ～ iOS 16.x
* Xcode 13.2.1 〜 Xcode 14.x
* armv7k, arm64, arm64e アーキテクチャ

SDKのバージョンアップを行う場合は[こちら（参考：SDKのアップデートについて）](https://mbaas.nifcloud.com/doc/current/introduction/quickstart_swift.html#Carthage%E3%82%92%E5%88%A9%E7%94%A8%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95)を参考に更新をお願いします。また、最新のSDKバージョン及び動作環境については[mobile backend Swift SDK | GitHub](https://github.com/NIFCLOUD-mbaas/ncmb_swift)をご参照ください。
プロジェクト作成時に動作検証を行った端末情報も併せて記載しておきます。

* macOS Monterey 12.6.1
* Xcode 14.0.1
* iOS 15.7 (iPhone 11 Pro)

※プッシュ通知の動作検証には実機ビルドが必須です

### 事前準備
* 証明書の取得
 * __[プッシュ通知に必要な証明書の作り方](http://qiita.com/natsumo/items/d5cc1d0be427ca3af1cb)__
* [ニフクラ mobile backend ](https://mbaas.nifcloud.com/) のアカウント取得（SNS ID で取得：無料）

## サンプルプロジェクト一覧
すべて自由に使ってOKです▼

|言語|プッシュ通知|リッチプッシュ|ペイロード取得|
|:-----------------:|:-----------------:|:-----------------:|:-----------------:|
|__Swift__|__[SwiftPushApp](https://github.com/natsumo/SwiftPushApp)__|__[SwiftRichPushApp](https://github.com/natsumo/SwiftRichPushApp)__|__[SwiftPayloadPushApp](https://github.com/natsumo/SwiftPayloadPushApp)__|

* 上記３つのサンプルを用意しました！
  * 通常のプッシュ通知のみ実装した、__SwiftPushApp__
  * SwiftPushAppに加えて、プッシュ通知を開くとWebViewを表示できるリッチプッシュ機能を実装した、__SwiftRichPushApp__
  * SwiftPushAppに加えて、受信したプッシュ通知からメッセージや独自設定データ（JSON）などのペイロードデータを取得する機能を実装した、__SwiftPayloadPushApp__
* 使い方は各リンク先のREADMEにしたがってください◎

# ちょこっと解説

細かい解説は各GitHub側のREADMEに記述しているのでそちらをご覧ください。ここでは簡単な仕組みや機能の説明、実装コードの一部をご紹介します。

## そもそもプッシュ通知の仕組みってどうなってるの？
iOSの場合はAPNsというApple社のプッシュ通知用サーバーを介してプッシュ通知は配信されます。
下図のように①～⑤の流れでプッシュ通知は端末に届きます。

![001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/112032/9f1e7624-774e-9db1-1093-2192f9a9cf26.png)


この中で「②デバイストークン発行」でAPNsから発行されたデバイストークンを端末で取得し、サーバー側に保存する処理は、アプリ側に実装する必要があります。

## デバイストークンの取得とサーバーへの登録する処理

```swift:AppDelegate
import UIKit
import NCMB
import UserNotifications

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        
        // APIキーの設定とSDK初期化
        NCMB.initialize(applicationKey: "YOUR_APPLICATION_KEY", clientKey: "YOUR_CLIENT_KEY")
        
        // APNsにデバイストークンを要求
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { (granted, error) in
            if((error) != nil) {
                return
            }
            if granted {
                DispatchQueue.main.async {
                    UIApplication.shared.registerForRemoteNotifications()
                }
            }
        }
        
        return true
    }
    
    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        //端末情報を扱うNCMBInstallationのインスタンスを作成
        let installation : NCMBInstallation = NCMBInstallation.currentInstallation

        //Device Tokenを設定
        installation.setDeviceTokenFromData(data: deviceToken)

        //端末情報をデータストアに登録
        installation.saveInBackground(callback: { result in
            switch result {
            case .success:
                //端末情報の登録が成功した場合の処理
                print("保存に成功しました")
            case let .failure(error):
                //端末情報の登録が失敗した場合の処理
                print("保存に失敗しました: \(error)")
                return;
            }
        })
    }

// 以下省略
}
```

## リッチプッシュの処理

![RichPush.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/112032/870465ef-013b-b189-eff1-d806cf386f47.png)

### リッチプッシュって何？

* プッシュ通知にURLを載せて配信し、プッシュ通知開封時にWebビューで表示することができる機能です
* 実装は簡単で、上記デバイストークン処理のコードに加え、下記のコード（2か所）を追記するだけです◎

### （アプリKILL状態→起動時）リッチプッシュを表示させる処理

`didFinishLaunchingWithOptions` メソッド内、`return true` の前に追記：

```swift:AppDelegate.swift
// MARK: リッチプッシュ通知を表示させる
if let notification = launchOptions?[.remoteNotification] as? [String: AnyObject] {
    NCMBPush.handleRichPush(userInfo: notification)
}
```

### （アプリバックグランド起動中）リッチプッシュを表示させる処理

```swift: AppDelegate.swift
// プッシュ通知を受信したときの処理
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any],fetchCompletionHandler completionHandler:@escaping (UIBackgroundFetchResult) -> Void) {
    if let notiData = userInfo as? [String : AnyObject] {
        // MARK: リッチプッシュ通知を表示させる
        NCMBPush.handleRichPush(userInfo: notiData)
    }
}
```

## プッシュ通知からペイロードを取得する

![PayloadPush.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/112032/a305bb7e-18a0-f04d-311b-669240c8735e.png)

### ペイロードって何？

プッシュ通知に含まれるデータのことです。プッシュ通知のタイトルやメッセージのみならず、プッシュ通知の設定状況などの情報も含まれています。

```json:ペイロードデータ
{
    "aps" : {
        "alert" : {
            "body" : "message",
            "title" : "title"
        },
        "content-available" : 1,
        "sound" : "default"
    },
    "com.nifcloud.mbaas.PushId" : "********",
    "userSettingJson" : "test",
}
```

### カスタムペイロードって何？

プッシュ通知の配信時に、タイトルやメッセージ以外に情報を持たせてプッシュ通知を送ることができる機能です。プッシュ通知には表示されない内容をアプリ側で受信できるため、遠隔でアプリに作用させることが可能です。

例としては、

- カスタムペイロードとして日時情報を持たせ、アプリ側で取得、取得した日時情報を元にローカルプッシュ通知を作動させる
- カスタムペイロードとして位置情報を持たせ、アプリ側で取得、取得した位置情報に近い場所にいるユーザーにのみにアプリ内コンテンツを表示する

など使えます。

mobile backend のプッシュ通知には、JSON形式の任意のデータ（カスタムペイロード）を設定して配信することができる機能があり、簡単にカスタムペイロードを使うことができます！

### （アプリKILL状態→起動時）プッシュ通知からペイロードデータを取得する処理

`didFinishLaunchingWithOptions` メソッド内、`return true` の前に追記：

```swift:AppDelegate.swift
// MARK: アプリが起動されたときにプッシュ通知の情報（ペイロード）からデータを取得する
if let payload = launchOptions?[.remoteNotification] as? [String: AnyObject] {
   let  payloadData = payload.map{(key, value) in "\"\(key)\":\"\(value)\""}.joined(separator: "\n")

    // 以下省略
}
```

### （アプリ起動中：フォアグラウンド・バックグランド）プッシュ通知からペイロードデータを取得する処理

```swift:AppDelegate.swift
// MARK: アプリが起動中にプッシュ通知を受信したとき、プッシュ通知の情報（ペイロード）からデータを取得する
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
    let payloadData = userInfo.map{(key, value) in "\"\(key)\":\"\(value)\""}.joined(separator: "\n")

    // 以下省略    
}
```