Firebase Cloud Messaging(FCM)でReactNativeアプリに通知機能を実装する（iOS編）

# はじめに

アプリと言えば通知機能、というわけでFirebase Cloud Messaging(FCM)を利用してiOSアプリの通知機能を作成していきたいと思います。iOSはAPN、AndroidはGCMと個別の技術で開発する方法もありますが、FCMを利用すればなんと共通のコードで通知機能を実現できます。


# 実装のゴール

ゼロからReactNativeのプロジェクトをはじめて、通知センターに通知が表示されて、通知のタップ情報をアプリで受け取れるところまでをゴールとして進めていきます。

# プロジェクトの作成

まずはinitコマンドでプロジェクトを作成します。

```
react-native init FCMNotification
```

## react-native-fcmパッケージの追加

[react-native-fcm](https://github.com/evollu/react-native-fcm)パッケージを利用して実装するので追加します。linkもしときましょう。

```
yarn add react-native-fcm
react-native link react-native-fcm
```

# Xcodeプロジェクトの設定変更

## Cocoapodsのセットアップ

※ cocoapodsがない場合は`gem install cocoapods`しておきましょう。

iOSのディレクトリに移動して、CocoapodsをXcodeプロジェクトで利用できる状態にします。

```
cd ios && pod init
```

続けて、以下のようにPodfileを書き換えます。

```ruby:Podfile
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'FCMNotification' do
  # Uncomment the next line if you're using Swift or would like to use dynamic frameworks
  # NOTE: コメントを外す
  use_frameworks!

  # Pods for FCMNotification
  # NOTE: Firebase/Messagingを追加
  pod 'Firebase/Messaging'

  target 'FCMNotificationTests' do
    inherit! :search_paths
    # Pods for testing
  end
end
```

## Xcodeプロジェクト内のコードの書き換え

続いて以下のコマンドでXcodeを開き、プロジェクト内のファイルを書き換えます。

```
open FCMNotification.xcworkspace
```

※ `pod init`によって`プロジェクト名.xcworkspace`というプロジェクトファイルが生成されます。間違えて`.xcodeproj`のプロジェクトファイルを開かないように注意。

AppDelegate.hを以下のように書き換えます。

```objectivec:AppDelegate.h
#import <UIKit/UIKit.h>

@import UserNotifications;

@interface AppDelegate : UIResponder <UIApplicationDelegate, UNUserNotificationCenterDelegate>

@property (nonatomic, strong) UIWindow *window;

@end
```

続けてAppDelegate.mを以下のように書き換えます。

```objectivec:AppDelegate.m
#import "AppDelegate.h"

#import "RCTBundleURLProvider.h"
#import "RCTRootView.h"

#import "RNFIRMessaging.h"

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  NSURL *jsCodeLocation;

  jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index.ios" fallbackResource:nil];

  RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
                                                      moduleName:@"FCMNotification"
                                               initialProperties:nil
                                                   launchOptions:launchOptions];
  rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1];

  self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
  UIViewController *rootViewController = [UIViewController new];
  rootViewController.view = rootView;
  self.window.rootViewController = rootViewController;
  [self.window makeKeyAndVisible];
  
  [FIRApp configure];
  [[UNUserNotificationCenter currentNotificationCenter] setDelegate:self];
  
  return YES;
}

- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler
{
  [[NSNotificationCenter defaultCenter] postNotificationName:FCMNotificationReceived object:self userInfo:notification.request.content.userInfo];
  if([[notification.request.content.userInfo valueForKey:@"show_in_foreground"] isEqual:@YES]){
    completionHandler(UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionBadge | UNNotificationPresentationOptionSound);
  }else{
    completionHandler(UNNotificationPresentationOptionNone);
  }
  
}

- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)())completionHandler
{
  NSDictionary* userInfo = [[NSMutableDictionary alloc] initWithDictionary: response.notification.request.content.userInfo];
  [userInfo setValue:@YES forKey:@"opened_from_tray"];
  [[NSNotificationCenter defaultCenter] postNotificationName:FCMNotificationReceived object:self userInfo:userInfo];
}

//You can skip this method if you don't want to use local notification
-(void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification {
  [[NSNotificationCenter defaultCenter] postNotificationName:FCMNotificationReceived object:self userInfo:notification.userInfo];
}

- (void)application:(UIApplication *)application didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo fetchCompletionHandler:(nonnull void (^)(UIBackgroundFetchResult))completionHandler{
  [[NSNotificationCenter defaultCenter] postNotificationName:FCMNotificationReceived object:self userInfo:userInfo];
  completionHandler(UIBackgroundFetchResultNoData);
}

@end
```

## アプリの通知機能を有効にする

プロジェクトの設定から通知関連の機能を有効にしておきます。これを忘れると通知が届かないので注意！

![FCMNotification_xcodeproj.png](https://qiita-image-store.s3.amazonaws.com/0/5877/b207d465-699d-519e-e285-c2d2822c8c52.png "FCMNotification_xcodeproj.png")

# Firebaseプロジェクトのセットアップ

続いて、Firebaseプロジェクトを作成して、Firebaseから通知を配信できるようにします。

## Firebaseプロジェクトを新規に作成する

![Firebase_Console.png](https://qiita-image-store.s3.amazonaws.com/0/5877/516523c6-ce3d-a34b-0592-44692949eb94.png "Firebase_Console.png")

FirebaseプロジェクトにiOSアプリを追加し、`GoogleService-Info.plist`をダウンロードします。

![Firebase_Console.png](https://qiita-image-store.s3.amazonaws.com/0/5877/ace3bf0a-6a14-6c4d-5a4c-5a0d9c999b49.png "Firebase_Console.png")

ダウンロードした`GoogleService-Info.plist`をXcodeプロジェクトに追加し、Copy Bundle Resourcesに追加します。

![FCMNotification_xcodeproj.png](https://qiita-image-store.s3.amazonaws.com/0/5877/b764247e-dcf0-c1f2-3b8a-25d39635fdea.png "FCMNotification_xcodeproj.png")


更に、FirebaseからAPNに連携するために、APNs証明書を設定しておきます。

![Firebase_Console.png](https://qiita-image-store.s3.amazonaws.com/0/5877/359905fa-7c4f-3658-7bcd-4b2708baf3a0.png "Firebase_Console.png")

※ APNs証明書を作成する際はfastlaneのpemを利用すると便利です（参考：[fastlane の pem を使って一瞬で iOS 用の Push 通知 (Apple Push Notification Service) の証明書を作る](http://qiita.com/mironal/items/7fab4bf3bf2243a5fc90)）

# ReactNative側のコードを書く

通知系の処理はNotificationというコンポーネントに切り分けています。

```js:index.ios.js
import React from 'react';
import { AppRegistry } from 'react-native';
import Root from './src/Root';

const FCMNotification = () => <Root />;
AppRegistry.registerComponent('FCMNotification', () => FCMNotification);
```

```js:src/Root.js
import React, { Component } from 'react';
import {
  View,
  Text,
  StyleSheet,
} from 'react-native';
import Notification from './Notification'

export default class Root extends Component {
  render() {
    return (
      <View style={styles.container}>
        <Notification />
        <Text>Notification Test</Text>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center'
  },
});
```

```js:src/Notification.js
import React, { Component } from 'react';
import { View } from 'react-native';
import FCM from 'react-native-fcm';

export default class Notification extends Component {
  componentDidMount() {
    FCM.requestPermissions(); // for iOS
    FCM.getFCMToken().then(token => console.log(`[TOKEN] ${token}`));
    // 通知タップ時の処理を書く
    this.notificationUnsubscribe = FCM.on('notification', notif => console.log(notif));
    // トークンがリフレッシュされたときの処理を書く
    this.refreshUnsubscribe = FCM.on('refreshToken', token => console.log(`[TOKEN] ${token}`));
  }

  componentWillUnmount() {
    this.notificationUnsubscribe();
    this.refreshUnsubscribe();
  }

  render() {
    return (<View />);
  }
}
```

FCMからのイベントは`FCM.on`で受け取って処理します。今回は受け取った情報をそのままログとして表示するようにしています。

# 通知のテスト

## アプリが起動するか確認

ここまでセットアップを終えると、こんなアプリが立ち上がるようになります（スクリーンショットはシミュレータで立ち上げたものです）。

![Glass.png](https://qiita-image-store.s3.amazonaws.com/0/5877/2817ba48-04cf-4ad2-058e-810b1cb5112b.png "Glass.png")


## Firebaseコンソールから通知のテスト

さて、ここから早速通知機能が動いているかどうかテストしていきたいと思います！

Firebaseコンソールから手動で通知を送信することができるので、まずは通知先アプリのトークンを取得します。

トークンを取得するためには実機でアプリを立ち上げる必要があるので、実機でアプリを立ち上げます。すると、以下のようにトークンの取得ログが表示されます。

![AppDelegate_m.png](https://qiita-image-store.s3.amazonaws.com/0/5877/7067b5a6-a40d-f135-e2fd-72b29004aecd.png "AppDelegate_m.png")

こちらのトークンをコピって、Firebaseコンソールから通知を送信します。

![Firebase_Console.png](https://qiita-image-store.s3.amazonaws.com/0/5877/1a2e3b6e-f97a-cf07-cdd7-d2e0d78f69da.png "Firebase_Console.png")

すると実機に通知が届きます。

![スクリーンショット_2016_12_13_13_10.png](https://qiita-image-store.s3.amazonaws.com/0/5877/706fc84d-9047-d1f2-5606-8d1472266057.png "スクリーンショット_2016_12_13_13_10.png")

「テスト2」の通知をタップすると、Xcodeのログに以下のような情報が表示されます。

![FCMNotification_xcodeproj.png](https://qiita-image-store.s3.amazonaws.com/0/5877/ca3b63fd-7a65-ba68-56b9-ef5c6223cad3.png "FCMNotification_xcodeproj.png")

```json
{'gcm.notification.sound2': 'default',
  'google.c.a.e': '1',
  'google.c.a.c_l': 'test2',
  aps: { badge: 1, alert: 'テスト2', sound: 'default' },
  opened_from_tray: true,
  'gcm.n.e': '1',
  'google.c.a.c_id': '1543238649561071055',
  'google.c.a.udt': '0',
  'gcm.message_id': '0:1481601928852855%c218bf06c218bf06',
  'google.c.a.ts': '1481601928' }
```

`FCM.on('notification')`イベントの引数として上記の情報が渡されるので、この情報を元にアプリ内の特定の場所に遷移したり、といった処理を書いていきます。

これで通知を動かすことができました！お疲れ様でした！

続けて次の記事でAndroid編も書こうと思います。