iOS: シミュレータで最小限で確認するAPNs(StoryBoard編)

# はじめに
iOSアプリ開発の際に、簡単にシミュレータでPUSH通知の挙動を確認したい時がある。
簡単に記事にまとめようと思ったが、有料のDeveloperアカウントを持っていない状態でどこまで確認できるのか単純に疑問に思ったのでやってみたw

無駄なようにも思えるが、Push Notificationsを有効にした証明書を作成せずに、本当にサクッと確認したい時はこの記事が生きてくるだろう

※尚、SwiftUIを採用するか、StoryBoardを使用するか実装方法が変わってくる。
　今回はStoryBoardを使用した場合の記事となる、SwiftUI使用の場合は別記事とする。

# 確認環境
・Mac OS Sequoia 15.2
・Xcode 16.2
・使用言語 Swift (バージョンは5)

・プロジェクト：InterfaceはStoryboardを採用
・シミュレータ：iPhone16(iOS18.2)

# 実装
## プロジェクト作成
今回はInterfaceはStoryboardを使用
![スクリーンショット 2024-12-21 11.08.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3782130/153902ca-fda1-80b9-61a4-75a8dd3a07e5.png)

## Signing & Capabilities
Background ModesでRemote notificationsを有効にする
![スクリーンショット 2024-12-21 11.13.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3782130/c2994fa2-0457-7647-763f-a211cc0b2ca2.png)

※本来はここでCapabilitiesにRemote Notificationを追加して有効にする必要があるが、無料のDeveloperアカウントを使用しているのでこの部分は省略

## AppDelegate
・アプリ起動時にPUSH通知の許可Dialogを表示し、ユーザーに許可を求める。
```swift:AppDelegate.swift
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        registerForPushNotifications()
        return true
    }

    /// ユーザーにPUSH通知の許可を要求（許可Dialogを表示）
    private func registerForPushNotifications() {
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) {
            (granted, error) in
            print("Permission granted: \(granted)")
            // 1. Check if permission granted
            guard granted else { return }
            
            // 2. Attempt registration for remote notifications on the main thread
            DispatchQueue.main.async {
                UIApplication.shared.registerForRemoteNotifications()
            }
        }
    }
```

・許可Dialogで許可されたらDeviceTokenを取得

```swift:AppDelegate.swift
    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
        print("APNs Device Token: \(token)")
    }
      
    func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
        // 以下エラーが検出
//        Failed to register: Error Domain=NSCocoaErrorDomain Code=3000 "アプリケーションの有効な“aps-environment”エンタイトルメント文字列が見つかりません" UserInfo={NSLocalizedDescription=アプリケーションの有効な“aps-environment”エンタイトルメント文字列が見つかりません}
        
        // 原因は以下（無料でPUSH通知を試す限界）
        //　・Push Notificationsを有効にしたアプリの証明書を組み込んいない
        //　・CapabilityでPush Notificationsを有効にしていない
        print("Failed to register: \(error)")
    }
```

## SceneDelegate
・PUSH通知のDelegateをハンドリングするため登録

```swift:SceneDelegate.swift
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
        // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
        // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
        guard let _ = (scene as? UIWindowScene) else { return }
        
        // iOS13以上はSceneDelegateをdelegate先に設定
        UNUserNotificationCenter.current().delegate = self

        // プッシュ通知による起動
        if let response = connectionOptions.notificationResponse {
            // userInfoから埋め込んだkeyを取得して特定の処理を行う（ここではprint文で吐き出すだけ）
            let userInfo = response.notification.request.content.userInfo
            print(userInfo)
        }
    }
```

・PUSH通知をタップした時のイベント、またフォアグラウンドでPUSH通知受信時のイベント
　注意点は以下コメントに記載されている通り

```swift:SceneDelegate.swift
// MARK: - UNUserNotificationCenterDelegate
@available(iOS 13.0, *)
extension SceneDelegate: UNUserNotificationCenterDelegate {

    // PUSH通知をタップした時にコールされるイベント
    // ※注意：バックグラウンドでもフォアグラウンドでも同様
    //       バックグラウンドでPUSH通知を受信した時にコールされるイベントはない
    //       またアプリ未起動の状態でPUSH通知を受信した時にコールされるイベントも同様にない
    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        // userInfoから埋め込んだkeyを取得して特定の処理を行う（ここではprint文で吐き出すだけ）
        let userInfo = response.notification.request.content.userInfo
        print(userInfo)
        
        completionHandler()
    }
    
    // フォアグラウンドでPUSH通知を受信した時にコールされるイベント
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        // .alertはiOS14でdeprecatedされているためwariningが検出されている（削除はされていない模様）
        completionHandler([.list, .banner, .badge, .sound])
    }
}
```

## シミュレータで確認する時のapnsファイルをローカルに用意
・最小限のapnsの構成で用意
　ここで注意が必要なのは、"Simulator Target Bundle"に使用するプロジェクトのBundle IDに随時書き換える必要があること

```swift:test.apns
{
    "Simulator Target Bundle": "test8.test8",
    "aps": {
        "alert": {
            "title": "通知テストタイトル",
            "body": "通知テストメッセージ"
        }
    }
}
```

## 動作確認
フォアグラウンドで受信できることを確認
通知をタップした後にイベントをハンドリングできることも確認
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3782130/0ebe835c-913d-aed3-b03d-c1664b4f2c49.png">

バックグラウンドで受信できることを確認
通知をタップした後にイベントをハンドリングできることも確認
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3782130/7505e224-2688-d429-058d-f2741ab3598f.png">

# 最後に
有料のDeveloperアカウントで、本来実施すべきPush Notifications機能を有効にした証明書をプロジェクトに取り込まないとPUSH通知の動作確認は出来ないと思っていました。

ですが、シミュレータで実験的に確認する分には
結果的に無料のDevelopアカウントでもPUSH通知の動作確認は可能なようです。

ただし、あくまでシミュレータで実験的に確認する話なので、
本番運用の場合はPush Notifications機能を有効にした証明書を取り込み、
かつ実機での動作確認は必須です。


以上