iOSアプリをプッシュ通知に対応させる手順まとめ

[CafeSnap](http://cafesnap.me)にPush Notificationsを追加するにあたり、対応＆実装した手順です。

# おおまかな流れ
1. member centerでAppIDごとの設定からpush通知を有効化
2. macのキーチェーンアクセスで証明証を発行
3. member centerでAPNsの証明証を発行
4. amazon snsに証明証を登録
5. クライアントサイドに通知を受け取るコードを実装
6. いったんテスト
7. サーバーサイドにプッシュ通知を送信する処理を実装
8. 完成

# 手順
## member centerでアプリのプッシュ通知の設定を追加する
下記の通りPush NotificationsがDisabledになっています。
![スクリーンショット 2015-10-07 18.31.01.png](https://qiita-image-store.s3.amazonaws.com/0/10532/489f8c09-36eb-01b6-bae1-72c5b3a2c802.png "スクリーンショット 2015-10-07 18.31.01.png")

Edit画面に移動し、Push Notificationsにチェックを入れます
![スクリーンショット 2015-10-07 18.35.14.png](https://qiita-image-store.s3.amazonaws.com/0/10532/2ce03cf4-4a4d-dc6b-0d04-614723dd209f.png "スクリーンショット 2015-10-07 18.35.14.png")

上記画面でチェックしてdoneを押すと、一覧画面では下記のようになります
![スクリーンショット 2015-10-07 18.39.52.png](https://qiita-image-store.s3.amazonaws.com/0/10532/fd0f3e23-6d62-174e-75a6-d3e1008503ad.png "スクリーンショット 2015-10-07 18.39.52.png")

## macで証明証を要求
キーチェーンappを起動して証明証を要求 
<Font color="red">※ここで発行する証明証は使いまわさず、DevとDistribution別々に作成してください。プッシュ通知が届かずハマります。。</Font>
![スクリーンショット 2015-10-07 18.50.25.png](https://qiita-image-store.s3.amazonaws.com/0/10532/5f84f8b4-b6d0-1905-5fb1-bdcbcfaa712f.png "スクリーンショット 2015-10-07 18.50.25.png")

開発者のメールアドレスを入力(CAのアドレスやその後に聞かれるパスワードは空でOK)。進むとファイルができます。
![スクリーンショット 2015-10-07 18.51.08.png](https://qiita-image-store.s3.amazonaws.com/0/10532/3797d233-f620-9f05-d50b-8eff6cb1293e.png "スクリーンショット 2015-10-07 18.51.08.png")

member centerでもう一度下記画面を表示してcreate certificateを行います
![スクリーンショット 2015-10-07 18.35.14.png](https://qiita-image-store.s3.amazonaws.com/0/10532/2ce03cf4-4a4d-dc6b-0d04-614723dd209f.png "スクリーンショット 2015-10-07 18.35.14.png")

Choose Fileをクリックして、先ほどディスクに保存した証明書をアップロードします
![スクリーンショット 2015-10-07 18.55.05.png](https://qiita-image-store.s3.amazonaws.com/0/10532/c02a5eb7-525e-08c0-35e6-be7ccdc3fe5e.png "スクリーンショット 2015-10-07 18.55.05.png")

下記画面からAPNsの証明証をダウンロードして開きます。
![スクリーンショット 2015-10-07 18.55.46.png](https://qiita-image-store.s3.amazonaws.com/0/10532/f8731071-e95a-9838-fcc2-f16633b32e6e.png "スクリーンショット 2015-10-07 18.55.46.png")

開くとキーチェーンアクセスに追加されます
![スクリーンショット 2015-10-07 18.57.35.png](https://qiita-image-store.s3.amazonaws.com/0/10532/d130cd70-e879-34aa-2d9c-ba14db7e4dbe.png "スクリーンショット 2015-10-07 18.57.35.png")

ダウンロードした証明証をp12ファイルとして書き出しておく
![スクリーンショット 2015-10-08 11.11.46.png](https://qiita-image-store.s3.amazonaws.com/0/10532/47d36167-59c7-9d99-0d3b-602dbf8aab83.png "スクリーンショット 2015-10-08 11.11.46.png")

### 上記が終了したら、ProvisioningProfileがinvalidになっているはずなので、edit→generateを行って下さい。
※上記はdev/pro両方行う必要があります。

## Amazon SNSの設定をする
### 下記作業の前に、SNS用のIAMユーザーを作成しておきます。
アプリケーションを作成。名前は特に決まりは無いですが、developmentとproductionが区別できる名前がよいです。
![スクリーンショット 2015-10-08 11.08.18.png](https://qiita-image-store.s3.amazonaws.com/0/10532/b3c3bb7b-d4d7-7641-dabb-fce9b5f67c74.png "スクリーンショット 2015-10-08 11.08.18.png")

Apple Developmentを選択し、先ほどのp12ファイルをアップロードします。作成時にパスワード指定した場合は入力します。
![スクリーンショット 2015-10-08 11.14.25.png](https://qiita-image-store.s3.amazonaws.com/0/10532/409403db-5fe8-553e-7cba-6f5f7fa17920.png "スクリーンショット 2015-10-08 11.14.25.png")

## 実装
流れとしては、
1. 端末でAPNsのデバイストークン取得
2. 自前のAPIサーバーにデバイストークンを送信
3. APIサーバーからAmazon SNSにデバイストークンを登録
4. プッシュ通知を送信

### iOS
今回はiOS8以降を想定

```objectivec
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
	...
	// プッシュ通知
    UIUserNotificationType types =
    UIUserNotificationTypeBadge|
    UIUserNotificationTypeSound|
    UIUserNotificationTypeAlert;
    UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:types categories:nil];
    [application registerUserNotificationSettings:settings];
	...
}

- (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings
{
    [application registerForRemoteNotifications];
}

- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(nonnull NSData *)deviceToken
{
    // スペースが入っているのでdeviceTokenを整形
    NSString *deviceTokenString = [[[NSString stringWithFormat:@"%@", deviceToken]
                                    stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"<>"]]
                                   stringByReplacingOccurrencesOfString:@" " withString:@""];
    
    // デバイストークンはUDに保存し、変更があったらサーバーに送信する
    NSLog(@"%@", deviceTokenString);
}

- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(nonnull NSError *)error
{
    NSLog(@"%@", error);
}

- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
    NSLog(@"%@", userInfo);
}
```

とりあえずここまでできたらAWSのマネジメントコンソール上からテストでメッセージを送信してみます。 
Endpointを選択し、Publish to Endpointを押す
![スクリーンショット 2015-10-15 16.35.58.png](https://qiita-image-store.s3.amazonaws.com/0/10532/3a33a160-c561-7035-17cf-afdabc688764.png "スクリーンショット 2015-10-15 16.35.58.png")

Jsonを選択し、Json message generatorを開いてテストメッセージを作成し、送信すると端末に届けば成功。
![スクリーンショット 2015-10-15 16.36.21.png](https://qiita-image-store.s3.amazonaws.com/0/10532/9e016aed-27dc-93fd-6f28-569ce8f27740.png "スクリーンショット 2015-10-15 16.36.21.png")


## サーバーサイド
エンドポイントの登録とメッセージ送信用の最低限のメソッドのみ。エラー処理は共通のtry/catchで処理するようにしています。
 
※PHP+Laravel+aws-sdk-php-laravelを使用。

```php
class NotificationHelper
{
    protected static function getAWSInstance()
    {
        return AWS::factory([
            'key' => 'hogehoge',
            'secret' => 'hogehoge',
            'region' => 'hogehoge'
        ])->get('sns');
    }
    
    /**
     * amazon snsにデバイスを登録する
     * 
     * @param string デバイストークン
     * @return string AWSから返ってきたエンドポイントのAmazonリソースネーム
     */
    public static function registerDevice($device_token, $platform_arn)
    {
        $result = static::getAWSInstance()->createPlatformEndpoint([
            // PlatformApplicationArn is required
            'PlatformApplicationArn' => $platform_arn,
            // Token is required
            'Token' => $device_token
        ]);
        
        return $result['EndpointArn'];
    }

    /**
     * 通知を送信する
     *
     * @param string メッセージ
     * @param User   通知対象のユーザーオブジェクト
     * @return なし
     *
     */
    public static function publishMessage($message, $arn)
    {
        // 個人に紐づく各デバイスに通知を送信する
        if (!empty($arn) && static::checkEndpointAttributes($arn)) {
            static::getAWSInstance()->publish([
                'Message' => $message, 
                'TargetArn' => $arn
            ]);
        }
    }

    /**
     * arnが有効かどうかをチェックする
     *
     * @param string メッセージ
     * @param string ARN
     * @return string/bool deviceToken/false
     *
     */
    public static function checkEndpointAttributes($endpoint_arn)
	{   
        // Exceptionが発生せず、EnableがTrueであればデバイスは有効とみなす
        try {
            $result = static::getAWSInstance()->getEndpointAttributes([
                'EndpointArn' => $endpoint_arn
            ]);
            return ($result['Attributes']['Enabled']) ? $result['Attributes']['Token'] : false;
        } catch (Aws\Sns\Exception\SnsException $e) {
            return false;
        }
	}
}
```
## 複数端末にまとめてpushしたい場合
登録されている端末一つ一つにお知らせを送ると膨大な時間がかかってしまいます。 
そこで、Amazon SNSにはTopicsという機能があるので、それを使用して一斉pushを行います。

### コンソール上でTopicsを作成
作成するとTopicのARNが発行されるのでメモっておきます。
![スクリーンショット 2015-11-10 19.41.04.png](https://qiita-image-store.s3.amazonaws.com/0/10532/9e5dc6b7-65f8-2136-e453-410e0646e091.png "スクリーンショット 2015-11-10 19.41.04.png")

### API側にTopicの購読処理を追加する
おそらく先ほど紹介した、エンドポイントの登録のタイミングで下記の処理を行うとよいかと思います。

```php:エンドポイントの登録処理メソッドを抜粋
public static function registerDevice($device_token, $platform_arn)
{   
    $sns = static::getAWSInstance();
                           
    // デバイス登録                                                                                                                                                         
    $result = $sns->createPlatformEndpoint([
        'PlatformApplicationArn' => $platform_arn,
        'Token' => $device_token
    ]); 

    // 一斉push用にtopicを購読しておく
    $sns->subscribe([
        'TopicArn' => 'TopicのARN',
        'Protocol' => 'application',
        'Endpoint' => $result['EndpointArn']
    ]); 

    return $result['EndpointArn'];
}
```

### Topicを購読しているエンドポイントにpush通知する
```php
public static function publishMessageToTopic($message)
{   
    static::getAWSInstance()->publish([
        'Message' => $message,
        'TopicArn' => 'TopicのARN'
    ]); 
}
```


## メモ
* Provisioningを作成し直すのを忘れていてしばらくハマった
* Sandbox環境だとちょこちょこDeviceTokenが無効になる
* シミュレータで仮のデバイストークンが発行されるライブラリとかもあるが、最初から実機でやるのをオススメする
* 本番環境でテストを行いたい場合は、TestFlightでアプリを配信すると本番用のAPNSに接続される


# 参考
http://www.lancork.net/2013/08/how-to-ios-push-first/ 
http://docs.aws.amazon.com/ja_jp/sns/latest/dg/mobile-push-apns.html