CapacitorとReactを使用したFCMによるiOSプッシュ通知機能の実装についての体験談

## 概要
これまでFlutterを用いて簡易的なiOSアプリ開発の経験はありましたが、Flutterの学習は時間がかかるため、JavaScriptを使用してiOSアプリを開発できる「Capacitor」に興味を持ち、その機能を試してみることにしました。

## 前提
- Apple Developer Program に加入済みであること。

## 実装プロセス
この記事では、Capacitorのドキュメントや他のウェブサイトを参考にしながら、Push通知機能を作成した流れの説明を行なっています。

### アプリの構築
ReactとCapacitorの初期化コマンドを使ってアプリを構築する方法について説明します。

**1. Reactアプリの作成**
```sh
npx create-react-app capacitor-tutorial
cd capacitor-tutorial
```

**2. Capacitorを導入**

```sh
npm i @capacitor/core
npm i -D @capacitor/cli
npx cap init
```

Capacitorアプリの初期設定では、プロジェクト名とアプリのPackage IDを入力します。
Package IDはFirebaseの設定にも使用されます。

**3. iOSのプロジェクトを作成**

```sh
npm i @capacitor/ios
npx cap add ios
```

### Firebaseの設定

**1. Firebaseの設定**
Firebaseの設定に関しては、以下のサイトを参照してください。

https://capacitorjs.jp/docs/guides/push-notifications-firebase#ios

**2. Firebase/Messagingを導入**

Podfileに `Firebase/Messaging` を追加

<details>
<summary>Podfile</summary>

```rb:Podfile
require_relative '../../node_modules/@capacitor/ios/scripts/pods_helpers'

platform :ios, '13.0'
use_frameworks!

# workaround to avoid Xcode caching of Pods that requires
# Product -> Clean Build Folder after new Cordova plugins installed
# Requires CocoaPods 1.6 or newer
install! 'cocoapods', :disable_input_output_paths => true

def capacitor_pods
  pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
  pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
  pod 'CapacitorPushNotifications', :path => '../../node_modules/@capacitor/push-notifications'
  pod 'CapacitorToast', :path => '../../node_modules/@capacitor/toast'
end

target 'App' do
  capacitor_pods
  # Add your Pods here
  pod 'Firebase/Messaging'
end

post_install do |installer|
  assertDeploymentTarget(installer)
end
```

</details>

### Reactでの実装
Reactを使った具体的な実装方法

**1. ライブラリをインストール**

```sh
npm i @capacitor/push-notifications @capacitor/toast
npx cap update ios
```

**2. App.jsを更新**

プッシュ通知を受け取れ、簡単に確認できるようにApp.jsを更新しました。
@capacitor/push-notificationsの詳細についてはまだ完全に理解していないため、主にコピペを使用しました。

以下のサイトのIonic Reactのコードを参考にしました。

https://enappd.com/blog/firebase-push-notification-in-ionic-react-capacitor/111/

<details>
<summary>App.js</summary>

```js:App.js
import React, { useEffect, useState } from 'react';
import logo from './logo.svg';
import './App.css';
import { PushNotificationSchema, PushNotifications, Token, ActionPerformed } from '@capacitor/push-notifications';
import { Toast } from "@capacitor/toast";

function App() {
  const nullEntry = []
  const [notifications, setnotifications] = useState(nullEntry);

  useEffect(()=>{
      PushNotifications.checkPermissions().then((res) => {
          if (res.receive !== 'granted') {
            PushNotifications.requestPermissions().then((res) => {
              if (res.receive === 'denied') {
                showToast('Push Notification permission denied');
              }
              else {
                showToast('Push Notification permission granted');
                register();
              }
            });
          }
          else {
            register();
          }
        });
  // eslint-disable-next-line react-hooks/exhaustive-deps
  },[])

  const register = () => {
      // Register with Apple / Google to receive push via APNS/FCM
      PushNotifications.register();

      // On success, we should be able to receive notifications
      PushNotifications.addListener('registration',
          (token: Token) => {
              showToast('Push registration success');
          }
      );

      // Some issue with our setup and push will not work
      PushNotifications.addListener('registrationError',
          (error: any) => {
              alert('Error on registration: ' + JSON.stringify(error));
          }
      );

      // Show us the notification payload if the app is open on our device
      PushNotifications.addListener('pushNotificationReceived',
          (notification: PushNotificationSchema) => {
              setnotifications((notifications: any) => [...notifications, { id: notification.id, title: notification.title, body: notification.body, type: 'foreground' }])
          }
      );

      // Method called when tapping on a notification
      PushNotifications.addListener('pushNotificationActionPerformed',
          (notification: ActionPerformed) => {
              setnotifications((notifications: any) => [...notifications, { id: notification.notification.data.id, title: notification.notification.data.title, body: notification.notification.data.body, type: 'action' }])
          }
      );
  }

  const showToast = async (msg: string) => {
      await Toast.show({
          text: msg
      })
  }

  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React.
        </a>
        <div>
          <h2>Push Notification</h2>
          <p>Notification Count: {notifications.length}</p>
        </div>
      </header>
    </div>
  );
}

export default App;
``` 
</details>

**3. webのコードをiOSに同期**

```
npm run build
npx cap sync
```

### Xcodeでの設定
Xcodeでの設定手順

**1. Xcodeを開く**

```
npx cap open ios
```

**2. 確認デバイスを実機に変更**

Runnerをsimulatorから実機に変更

**3. Teamを選択**

「Targets > App > Signing & Capabilities」で「Team」を選択します（デフォルトはNone）

**4. Capabilityを追加**

「Targets > App > Signing & Capabilities」で「+ Capability」を選択し、Push Notificationを追加します。

### 実機での確認

1. Xcodeで「Run」を選択し、実機を起動します。
2. FirebaseのMessaging APIでキャンペーンを1件作成します。
3. しばらくするとメッセージが届き、アプリを開いているとNotification Countが1増えます。

### 推奨ライブラリへの切り替え
[ドキュメント](https://firebase.google.com/docs/ios/setup?hl=ja#add-sdks)によれば、推奨されるライブラリが変更されたため、以下のように変更します。

```diff:Podfile
target 'App' do
  capacitor_pods
- pod 'Firebase/Messaging'
+ pod 'FirebaseCore'
+ pod 'FirebaseMessaging'
end
```

```diff:AppDelegate.swift
　　　　import UIKit
　　　　import Capacitor
- import Firebase
+ import FirebaseCore
+ import FirebaseMessaging
```

## 遭遇した問題と解決策
**Firebase SDK の重複**

iOSの設定に不慣れな中、Firebaseのドキュメントに従って設定した後、「FirebaseMessaging」を追加すると、Swift Package ManagerとCocoaPodsでのインストールが干渉し、エラーが発生しました。

```
[FirebaseCore][I-COR000003] The default Firebase app has not yet been configured. Add `FirebaseApp.configure()` to your application initialization. This can be done in in the App Delegate's application(_:didFinishLaunchingWithOptions:)` (or the `@main` struct's initializer in SwiftUI). Read more: https://goo.gl/ctyzm8.
```

このエラーに関する検索では、「GoogleService-Info.plistが正しく配置されていない」「Firebase.configure()が正しく記述されていない」といった内容の記事が多く、時間の浪費となりました。

新規にアプリを作成し直したところ問題なく動作し、SDKを2つ追加するとこのエラーが発生することが分かりました。

## 参考にしたサイト

https://capacitorjs.jp/docs/guides/push-notifications-firebase

https://firebase.google.com/docs/cloud-messaging/ios/client?hl=ja

https://zenn.dev/feb19/articles/c3e6ec545383bb

https://firebase.google.com/docs/ios/setup?hl=ja

https://enappd.com/blog/firebase-push-notification-in-ionic-react-capacitor/111/

https://repro.io/contents/ios-remote-push-notifications-in-a-nutshell/