AWS SNSを使ってiOSへpush通知


※現在iOSのバージョンupにつき、記事修正中です。
コメントにてエラーが報告されていますので、あくまでご参考までにお願いいたします。

この記事では、AWSからiOS端末へRemotePush通知を送る方法について解説を行います。
iOSではSwift、サーバーサイドではRubyでコードを記述しています。またローカライズにも対応させています。


##参考サイト


https://github.com/aws/aws-sdk-ios
http://akisute.com/2014/08/apple-push-notification-apn-delegate.html


##プロジェクト作成


ひとまずXCodeにてプロジェクトを作成しておきます。Project名は"apnsTest"としました。ちなみにiOS7以上を想定しています。

<img src="https://qiita-image-store.s3.amazonaws.com/0/49796/eb5f497b-e4b7-1e84-2d1b-4ff979be3d96.png" alt="01" width="700">

またAWSと連携を行う為、AWS SDKのライブラリを使用します。`Podfile`に以下のコードを追加して、インストールしてください。CocoaPodsについてよくわからない方は[こちら](http://qiita.com/satoken0417/items/479bcdf91cff2634ffb1)を参考にしてみてください。

```Podfile:Podfile

pod 'AWSCore'
pod 'AWSAutoScaling'
pod 'AWSCloudWatch'
pod 'AWSDynamoDB'
pod 'AWSEC2'
pod 'AWSElasticLoadBalancing'
pod 'AWSKinesis'
pod 'AWSS3'
pod 'AWSSES'
pod 'AWSSimpleDB'
pod 'AWSSNS'
pod 'AWSSQS'
pod 'AWSCognito'

```

インストールが終わったら、Bridging-Headerにimportするヘッダーファイルを記述します。Bridging-Headerについてよく分からない方は[こちら](http://qiita.com/skatata/items/1facd024d239b9545031)を参考にしてください。

```Swift:apnsTest-Bridging-Header.h

#import <AWSCore/AWSCore.h>
#import <AWSS3/AWSS3.h>
#import <AWSDynamoDB/AWSDynamoDB.h>
#import <AWSSQS/AWSSQS.h>
#import <AWSSNS/AWSSNS.h>
#import <AWSCognito/AWSCognito.h>

```



##APNsのための証明書&ProvisiongProfile発行


続いてMemberCenterにて証明書を発行します。

### AppIDの作成

まずはAppIDの作成から行います。
App Nameは適当な名前でも大丈夫ですが、Explicit App IDにはXCodeのBundleIdentifierと同様の値を入力します。

![appID01.png](https://qiita-image-store.s3.amazonaws.com/0/49796/9b3d423f-da51-f0e9-0753-591ea0c42e19.png "appID01.png")

App Servicesのpush Notificationにも忘れずにチェックを入れます。
![appID02.png](https://qiita-image-store.s3.amazonaws.com/0/49796/388cddc3-d22b-6a7e-57e8-627547af0b93.png "appID02.png")

あとは流れに沿って作成します。

###APNsの証明書作成

続いて以下の画面で右上の+ボタンを押して新たな証明書を作ります。

![certificate.png](https://qiita-image-store.s3.amazonaws.com/0/49796/690db9a4-9fdf-c8ac-cd99-ed1aae3219d7.png "certificate.png")

今回はDevelopment版なので、"Apple Push Notification service SSL (Sandbox)"にチェックを入れてContinueボタンを押します。
![cirtificate2.png](https://qiita-image-store.s3.amazonaws.com/0/49796/76a20106-29b2-0fee-880c-47820cda6360.png "cirtificate2.png")

そして先ほど作成したAppIDを選択します。

![certificate03.png](https://qiita-image-store.s3.amazonaws.com/0/49796/81d5c87a-8844-c162-a158-b1218b63fe3c.png "certificate03.png")


###認証局に証明書を要求

ここでアップロードするためのCSRファイルを作成します。KeyChain Access Appを開いて
キーチェーンアクセス>証明書アシスタント>認証局に証明書を要求
をすると以下のように証明書情報が出てきますので、AppDeveloperに登録したメールアドレスと名前を入力し"ディスクに保存"にチェックを入れ、"続ける"を押し適当な場所に保存します。

![certificate04.png](https://qiita-image-store.s3.amazonaws.com/0/49796/fa8e32e5-f9af-e38c-a870-e5fd6ab432cb.png "certificate04.png")

作成したCSRファイルをアップロードし、しばらくするとcerファイルがダウンロードできる画面に移行しますので、ダウンロードします。ダウンロードしたcerファイルをダブルクリックすると、keychain Access Appが開き、自動的に証明書が登録されます。

### Provisioning Profile作成

アプリでAPNsを取得するにはDevelopment環境でも固有のProvisioningProfileが必要になります。"Provisioning Profiles"から右上の+ボタンを押し、DevelopmentのProvisioningProfileを作成します。App IDは先ほど作成したものを選択し、あとは流れに沿って最終的にダウンロードします。

![certificate06.png](https://qiita-image-store.s3.amazonaws.com/0/49796/2779e08f-d6c7-27e6-de9d-1cdde95e31d5.png "certificate06.png")


##AWS SNS

続いてAWS上でAPNsを実行するための手続きを行います。

###IAM(Identity and Access Management)

最初にAWSのサービス一覧からIAMサービスを選択し、AWS SNSのリソースにアクセスするための権限を取得します。左ペインのUsersから"Create New Users"をクリックします。

![aws01.png](https://qiita-image-store.s3.amazonaws.com/0/49796/c2c7d19e-9055-c15e-74d7-17c9918f850a.png "aws01.png")

そうするとuser作成の画面が出てきますので、適当に名前をつけます。

![aws05.png](https://qiita-image-store.s3.amazonaws.com/0/49796/6358cc85-8685-e365-c91e-fbc1b6c59c9e.png "aws05.png")

作成に成功するとAccessKeyとSecretKeyを発行してくれるので、ダウンロードします。ダウンロードする機会は１度きりらしいので、必ず落としましょう。

![aws06.png](https://qiita-image-store.s3.amazonaws.com/0/49796/16bc46a8-5dbd-f217-5a9c-4432d8b3f823.png "aws06.png")

次に作成したユーザーに、SNSに関する制御の権限を与えます。
作成したユーザーをクリックし、PermissionのAttach Policyをクリックします。Policy一覧が表示されますので、"AmazonSNSFullAccess"の項目にチェックを入れて"Attach Policy"をクリックします。
![aws08.png](https://qiita-image-store.s3.amazonaws.com/0/49796/fab6b5ef-1d97-4edb-b574-10152ba3c9d1.png "aws08.png")

以下のように権限が与えられます。

![aws04.png](https://qiita-image-store.s3.amazonaws.com/0/49796/34661d05-126d-76a9-dcca-5c3ec3d83f50.png "aws04.png")


### SNSのplatform application作成

さて、続いてSNSの"Platform application"を作成します。AWSのサービス一覧からSNSを選択します。ちなみに"platform application"はAWSからAppleのサーバーにpush通知を依頼するためのものです。
SNS Home>Create platform application
を選択すると以下のような画面が出てきます。

![aws11.png](https://qiita-image-store.s3.amazonaws.com/0/49796/17d7f670-0900-2eb2-0c15-bc29eca8d764.png "aws11.png")

"Application name"には適当な名前を入れます。
"Push notification platform"は、今回はiOSのdevelopment版なので、"Apple Push Notification Service Sandbox(APNS_SANDBOX)"を選択します。ちなみにProduction版の場合は"Apple Push Notification Service"を選択することになると思います(多分ですが)。
ここで、先ほどMemberCenterで作成したAPNsの証明書をAWSに渡します。再び"Keychain Access"を開き、作成した証明書を右クリックし、"書き出す"を選択します。

![aws10.png](https://qiita-image-store.s3.amazonaws.com/0/49796/59403347-da81-0cdc-1ed1-33ff162eed08.png "aws10.png")

証明書の名前は適当でいいのですが、日本語ではなくローマ字にしておいた方が無難のようです。同時にパスワードも求められますが、こちらも適当でOKです。作成に成功するとp12ファイルが出来上がるので、こちらを"Choose P12 File"で選択します。"Enter Password"にはp12ファイル作成時と同じパスワードを入力します。最後にLoad Credentials from Fileをクリックすると、それぞれのKey欄に情報が自動的に入力されます。

![aws09.png](https://qiita-image-store.s3.amazonaws.com/0/49796/2457b20e-5c78-8762-3a51-76a5438117e0.png "aws09.png")

作成が完了すると以下のように、ARN(Amazon Resource Name)が割り振られます。

![aws12.png](https://qiita-image-store.s3.amazonaws.com/0/49796/839b6618-b693-7198-d7b5-838672866f57.png "aws12.png")


ようやく下準備が終わりました。

## iOS側実装その１

さて、まずはRemotePush通知を受け取るためにCapabilitiesの"Background Modes"をオンにして、"Background fetch"と"Remote notification"にチェックを入れます。

![ios01.png](https://qiita-image-store.s3.amazonaws.com/0/49796/d63a7bc0-81f8-5936-b2b7-20cd9e8cb93f.png "ios01.png")

また固有のProvisioningProfileを指定する必要があるので、先ほどダウンロードしたProvisioningProfileを指定します。

![ios02.png](https://qiita-image-store.s3.amazonaws.com/0/49796/3a10a6fb-0958-26b6-343c-1fadf54c46ca.png "ios02.png")




続いて`AppDelegate.swift`に以下のようにRemotePushを受け取るための依頼処理を`application:didFinishLaunchingWithOptions`に記述します。iOS7と8では処理コードが異なりますので、場合分けしてあります。


```swift:AppDelegate.swift

	func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        
        let osVersion = UIDevice.currentDevice().systemVersion
        if osVersion < "8.0" {
            //iOS7
            application.registerForRemoteNotificationTypes( UIRemoteNotificationType.Badge | UIRemoteNotificationType.Sound | UIRemoteNotificationType.Alert )
        }
        else {
            //iOS8以降
            let settings = UIUserNotificationSettings(forTypes: UIUserNotificationType.Badge | UIUserNotificationType.Sound | UIUserNotificationType.Alert, categories: nil)
            application.registerUserNotificationSettings(settings)
            application.registerForRemoteNotifications()
        }

        return true
    }

```

さらに`AppDelegate.swift`にDelegateを追加します。以下の２つを追加してください。



```Swift:AppDelegate.swift

    func application(application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: NSError) {
        println(error)
    }
    
    func application(application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: NSData) {
        
    }


```


１つ目はエラー時、２つ目は依頼成功時に呼ばれるDelegateになっています。成功するとAppleのサーバーからDeviceTokenが送られてくるので、２つ目の関数に処理を記述していきますが、その前にAWSのprotocolを追加しておきましょう。`AWSCredentialsProvider`protocolを追加し、また`accessKey`と、`secretKey`をAppDelegateの変数に追加しておきます。

```Swift:AppDelegate.swift

class AppDelegate: UIResponder, UIApplicationDelegate,AWSCredentialsProvider {

    var window: UIWindow?
    //変数accessKey,secretKeyを追加
    var accessKey:String?
    var secretKey:String?

	...

```


続いて依頼成功時に呼ばれる関数`application:didRegisterForRemoteNotificationsWithDeviceToken`に必要な処理を記述します。

```Swift:AppDelegate.swift


	func application(application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: NSData) {
        println(deviceToken);
        
        //①DeviceTokenを成形
        let deviceTokenString = "\(deviceToken)"
            .stringByTrimmingCharactersInSet(NSCharacterSet(charactersInString:"<>"))
            .stringByReplacingOccurrencesOfString(" ", withString: "")
        
        //DeviceTokenを端末に保存
        NSUserDefaults.standardUserDefaults().setObject(deviceTokenString, forKey: "deviceToken")
        
        //AWS SNSへ接続するためのKey設定
        self.accessKey = "IAMで取得したAccessKeyを設定"
        self.secretKey = "IAMで取得したSecretKeyを設定"
        
        //customUserDataに保存するために、端末の言語情報をjson形式で記述
        let languages = NSLocale.preferredLanguages()
        let lang = languages.first as String!
        let json = "{\"lang\":\"\(lang)\"}"
        
        let serviceConfiguration = AWSServiceConfiguration(region:AWSRegionType.APNortheast1, credentialsProvider: self) //regionはAWS SNSで作成したApplicationのARNに記述してあるものと同様のTypeを選択

		 AWSServiceManager.defaultServiceManager().defaultServiceConfiguration = serviceConfiguration
        let sns = AWSSNS.defaultSNS()
        
        let input = AWSSNSCreatePlatformEndpointInput()
        input.token = deviceTokenString //DeviceTokenを設定
        input.customUserData = json     //任意の情報を設定(今回は言語情報を記述)
        input.platformApplicationArn = "AWS SNSで作成したApplicationのARNを設定"
        
		 //②AWS SNSへ端末情報を登録
        sns.createPlatformEndpoint(input).continueWithExecutor(BFExecutor.mainThreadExecutor(), withBlock: { (task: BFTask!) -> AnyObject! in
            if task.error != nil {
                println("Error: \(task.error)")
            } else {
                //AWS SNSに端末情報(Endpoint)の登録が成功すると、自身の端末のARN(endpointArn)がSNSから送られてくる
                let createEndpointResponse = task.result as AWSSNSCreateEndpointResponse
                println("endpointArn: \(createEndpointResponse.endpointArn)")
                
                //割り振られたEndpointArnを保存
                NSUserDefaults.standardUserDefaults().setObject(createEndpointResponse.endpointArn, forKey: "endpointArn")
            }  
            return nil
        })
    }

```


①まずはDeviceTokenを`NSUserDefaults`等を使って保存しておきます。ただ送られてきたDeviceTokenはそのままでは余計なものが含まれているので、適切な形に成形して保存します。次に送られてきたDeviceTokenをAWS SNSへ登録します。IAMで取得したAccessKeyとSecretKeyをそれぞれ設定して、AWSに接続できるようにします。
②AWS SNSにDeviceTokenを含めた端末情報を送信します。今回端末情報には言語設定の情報も一緒に送信しています。この情報を送ることで、サーバー側からローカライズされたデータ(文字)を送ることができるようになります。詳しくは後述します。
またinput.platformApplicationArnにはAWS SNSで作成したapplicationのArnを設定します。そして登録が成功すると、クロージャのBFTaskにはAWS SNSから割り振られたEndpointArnが入っています。こちらもDeviceTokenと同様に`NSUserDefaults`などで保存しておきましょう。EndpointArnを保持することで、AWS SNSのCustomUserDataの更新が可能になります。例えば端末の言語情報が変更された場合、以下のようにしてAWS SNS上のCustomUserDataを更新することができます。


```Swift:AppDelegate.swift
	 
    //CustomUserDataはendpointArnがあれば更新可能.
    func updateCustomUserDataForSNS() {
        
        let endpointArn = NSUserDefaults.standardUserDefaults().stringForKey("endpointArn")
        var serviceConfiguration = AWSServiceConfiguration(region: .APNortheast1, credentialsProvider: self)
        AWSServiceManager.defaultServiceManager().defaultServiceConfiguration = serviceConfiguration
        let sns = AWSSNS.defaultSNS()
        
        let languages = NSLocale.preferredLanguages()
        let lang = languages.first as String!
        let json = "{\"lang\":\"\(lang)\"}"
        
        let input = AWSSNSSetEndpointAttributesInput()
        //CustomUserDataを設定
        input.attributes = NSDictionary(object: json, forKey: "CustomUserData")
        input.endpointArn = endpointArn
        
        //SNSに端末情報を送信
        sns.setEndpointAttributes(input).continueWithSuccessBlock { (task) -> AnyObject! in
            return nil
            }.continueWithBlock { (task:BFTask!) -> AnyObject! in
                if let error = task.error {
                    println(error)
                }
                return nil
        }
    }

```


では実機で動かしてみます。以下のようにpush通知の確認画面が出てOKボタンを押した後、`application:didRegisterForRemoteNotificationsWithDeviceToken`が呼ばれていれば成功です。
<img src="https://qiita-image-store.s3.amazonaws.com/0/49796/4e9366ae-1882-246d-2b98-469eb9526d58.png" alt="01" width="300">

AWS SNSでも確認してみましょう。作成したApplicationのEndpointsに、登録されたデバイスの情報が１つあるはずです。またUser Dataのカラムにも言語情報が追加されました。

![ios04.png](https://qiita-image-store.s3.amazonaws.com/0/49796/e6d6b21d-c41c-cc91-589c-457a4f00f901.png "ios04.png")


##サーバー側実装

さて今度はサーバー側に処理コードを記述し、実際にpush通知を飛ばします。前述しましたが、本記事ではrubyで実装します。

まずはruby向けのAWS SDKを`gem`でインストールします。また今回はjsonも使用するので同様にinstallします。

```
gem install aws-sdk
gem install json
```

installができたら以下のように処理コードを書いていきます。


```ruby:publish.rb

require 'aws-sdk-v1'
require 'json' 

#①accessKeyとsecretKeyを設定
aws_access_key = 'IAMで取得したaccessKeyを設定'
aws_secret_key = 'IAMで取得したsecretKeyを設定'
region = 'ap-northeast-1' #platform applicationと同様のregionを設定

AWS.config(
	access_key_id:aws_access_key,
	secret_access_key:aws_secret_key,
	region:region
)

sns = AWS::SNS.new
client = sns.client

#②AWS SNSで作成したApplicationのArnを設定
endpoints = client.list_endpoints_by_platform_application(
  platform_application_arn: 'AWS SNSで作成したApplicationのArnを設定',
)

#それぞれのEndpointの処理
endpoints[:endpoints].each do |endpoint|

	#③endpointのcustomDataを参照
	customUserData = endpoint[:attributes]["CustomUserData"]
	json_info = JSON.parse(customUserData)
	
	alert_message = ""
	body_title = ""
	body_message = ""
	body_button_positive_string = ""
	body_button_negative_string = ""

	#userDataの情報からローカライズ分岐
	if json_info["lang"] == "ja" then
		alert_message = "おはようございます"
		body_title = "朝です"
		body_message = "今日もいい天気ですね"
		body_button_positive_string = "はい"
		body_button_negative_string = "いいえ"

	else
		alert_message = "Good Morning"
		body_title = "It's morning"
		body_message = "It's nice weather again today"
		body_button_positive_string = "YES"
		body_button_negative_string = "NO"
	end

	#============ここからメッセージ===============
	apns_payload = {
					#apsには決められた形式で記述を行う
					"aps" => { 
						"alert" => alert_message, #通知バーに表示される文字　コメントアウトするとサイレント通知モードになる
						"sound" => 'default', #通知が来た時になる音. カスタム音も流せる
						"content-available" => 1, #バッククラウンド処理を有効化
						"priority" => 10 #これを書いておくことで通知の成功率がupする.デフォでは5
					},
					# 以下に任意の情報を入れていく.
					"body" => {
						"title" => body_title,
						"message" => body_message,
						"positiveButton" => body_button_positive_string,
						"negativeButton" => body_button_negative_string
					}
					}.to_json
	message = { "default" => "default", "APNS_SANDBOX" => apns_payload }.to_json
	#============ここまでメッセージ===============

	client.publish(
		target_arn: endpoint[:endpoint_arn],
		message: message,
		message_structure: 'json'
		)
end

```

いろいろと書いてありますが、大事なところは
①accessKeyとsecretKeyの設定
②applicationArnの設定
③CustomUserDataの活用
くらいだと思います。
①はiOS側で記述した理由と同様、AWS SNSのデータを扱うための権利を与えてあげます。②ではどのapplicationにpush通知を依頼するかを指定しています。そして③ですが、iOS側からあらかじめ端末の言語情報を取得することで、push通知を送る段階でローカライズが可能になります。
また"メッセージ"の部分に関しては、"aps"に決められた形式で記述を行う必要がありますが、その外側では自由にデータの記述が可能です。ただしデータ(ペイロード)の最大サイズは256byteとなっているので、注意が必要です。 apsの詳細に関しては[こちら](http://akisute.com/2014/08/apple-push-notification-apn-delegate.html)を参考にしてみてください。以上でサーバー側の準備は完了です。


##iOS側実装その２

さてサーバー側ではpush通知を飛ばす準備ができましたが、iOSの方では肝心のpush通知を受け取るDelegateを記述していなかったので、以下のように記述します。今回は受け取ったpush通知のデータを元にAlertViewを表示させてみます。

```Swift:AppDelegate.swift

	//RemotePush通知取得関数
    func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject], fetchCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) {
        
        println(userInfo)
        //userInfoにはサーバー側で記述した"apns_payload"が格納されている
        if let customData = userInfo["body"] as? NSDictionary {
            var alertView = UIAlertView()
            alertView.title = customData["title"] as NSString
            alertView.message = customData["message"] as NSString
            alertView.addButtonWithTitle(customData["positiveButton"] as String!)
            alertView.addButtonWithTitle(customData["negativeButton"] as String!)
            alertView.cancelButtonIndex = 1
            alertView.delegate = self
            alertView.show()
        }
        completionHandler(.NoData)
    }

```

##Push通知

ようやくすべての準備が揃いましたので、サーバーからpush通知を飛ばしてみます。

```
ruby publish.rb
```
と入力し、以下のように通知、およびAlertViewが出れば成功です。お疲れさまでした。

<img src="https://qiita-image-store.s3.amazonaws.com/0/49796/c83bb9e9-026e-5ebb-e349-a7a1f2fce386.png" alt="01" width="300">
<img src="https://qiita-image-store.s3.amazonaws.com/0/49796/8e73cbb4-2c7e-eec8-b483-fb1181483511.png" alt="01" width="300">