Swiftでプッシュ通知を送ろう！

__【お知らせ】Swift3に対応しました！（2017/02/24更新）__

# 【Swift×mBaaS】シリーズ概要
* Swiftで[ニフティクラウドmobile backend](http://mb.cloud.nifty.com/)(通称：mBaaS)を使ってSwiftで開発を始めたい人向けのドキュメントです
* [ニフティクラウドmobile backendのiOS(Objectiv-C)用ドキュメント](http://mb.cloud.nifty.com/doc/current/index.html)を__Swift__用に書き換えて動かしてみたものをまとめました
* 初心者でもわかりやすいよう心掛けて作っていますが、わかりにくい部分がありましたらコメントをいただければ訂正しますのでお気軽にご意見をお願いします^^*
* 今回は__＜プッシュ通知編＞__です！
* プッシュ通知を送るには認証のため、**証明書**などのファイルを作成する必要があります
* 今回は必要な証明書類が揃っている状態で、アプリに実装していく部分を取り上げています
* 「証明書ってどうやって作るの？」という方は[プッシュ通知に必要な証明書の作り方](http://qiita.com/natsumo/items/d5cc1d0be427ca3af1cb)をご覧ください！

# __Swiftでプッシュ通知を送ろう！__
　プッシュ通知を送るにはAppleが提供している__APNs__([Apple Push Notification Service](https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/ApplePushService.html)の略で、サーバーからの通知をiPhoneなどの端末に転送するAppleのプッシュ通知を送るためのサービスのこと)と連携させる必要があります！①～⑤の流れでプッシュ通知は送信されます

![095.png](https://qiita-image-store.s3.amazonaws.com/0/112032/29e2cecd-c01c-39a0-2044-6592145b83e6.png)


## 準備するもの・使うもの
* Mac OS X 10.10(Yosemite)以上
* Xcode7以上 
 * Ver. 7.2.1で動作確認しています
 * Ver. 8.2.1で動作確認しています
* iPhone
 * Xcodeのバージョンと対応するバージョンにする必要があります
 * 接続用にLightningケーブルが必要です
* [AppleDeveloperProgram](https://developer.apple.com/programs/jp/) への有償登録
* __[プッシュ通知に必要な証明書の作り方](http://qiita.com/natsumo/items/d5cc1d0be427ca3af1cb)__を確認して、下記証明書類を作成
 * 開発者用証明書(.cer)
 * APNs用証明書(.p12)
 * プロビショニングプロファイル

## 手順
1. Xcode上にSwiftアプリを作成する
1. mBaaS上にアプリを作成し、APNs用証明書の設定を行う
1. デバイストークン取得のロジックをSwiftで書く！
1. 開発者用証明書とプロビショニングプロファイルを設定して、実機デバッグビルドを行う
1. 動作確認！プッシュ通知を送りましょう★

# 操作
## 1. Xcode上にSwiftアプリを作成する
* 新規でも既存のアプリでも大丈夫ですが、**SDKのインストールなど事前準備が必要**になります
* 下記ページご覧いただき操作をお願いします
 * [＜CocoaPods＞SwiftでmBaaSを始めよう！(use_framewoks!を有効にした方法)](http://qiita.com/natsumo/items/57d3a4d9be16b0490965)

## 2. mBaaS上にアプリを作成し、APNs用証明書の設定を行う
* [ニフティクラウドmobile backend](http://mb.cloud.nifty.com/)に1.で作成済みのアプリのダッシュボードを開きます
* 「アプリ設定」＞「プッシュ通知」をクリックすると設定画面が表示されます
* 「プッシュ通知」の「プッシュ通知の許可」のところの「許可する」にチェックをつけて、「保存する」をクリックします
* 「iOSプッシュ通知」の「証明書(p12)」のところの「証明書の選択」をクリックしてAPNs用証明書(.p12)をアップロードしてください
* 「証明書は設定されています」と表示されればOKです

![図F14.png](https://qiita-image-store.s3.amazonaws.com/0/112032/b4b55124-6761-9a33-a242-9143ed4e19d0.png)

## 3. デバイストークン取得のロジックをSwiftで書く！
### 配信端末情報の登録
　AppDelegateファイル内のapplication:didFinishLaunchingWithOptionsメソッド内でデバイストークンの要求をAPNsに対して行います

* application:didFinishLaunchingWithOptionsメソッド内に下記内容を記述します
 * 書き込む場所がよくわからない場合は[Swiftでデータベースを使おう！①（保存・取得・更新・削除）](http://qiita.com/natsumo/items/c00cf7a48e0f8cd8d236)の初期設定を参照してください

```swift:Swift3_AppDelegate.swift
// デバイストークンの要求
if #available(iOS 10.0, *){
    /** iOS10以上 **/
    let center = UNUserNotificationCenter.current()
    center.requestAuthorization(options: [.alert, .badge, .sound]) {granted, error in
        if error != nil {
            // エラー時の処理
            return
        }
        if granted {
            // デバイストークンの要求
            UIApplication.shared.registerForRemoteNotifications()
        }
    }
} else {
    /** iOS8以上iOS10未満 **/
    //通知のタイプを設定したsettingを用意
    let setting = UIUserNotificationSettings(types: [.alert, .badge, .sound], categories: nil)
    //通知のタイプを設定
    application.registerUserNotificationSettings(setting)
    //DevoceTokenを要求
    UIApplication.shared.registerForRemoteNotifications()
}
```

```swift:Swift2_AppDelegate.swift
// デバイストークンの要求
if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_7_1){
    /** iOS8以上 **/
    //通知のタイプを設定したsettingを用意
    let type : UIUserNotificationType = [.Alert, .Badge, .Sound]
    let setting = UIUserNotificationSettings(forTypes: type, categories: nil)
    //通知のタイプを設定
    application.registerUserNotificationSettings(setting)
    //DevoceTokenを要求
    application.registerForRemoteNotifications()
}else{
    /** iOS8未満 **/
    let type : UIRemoteNotificationType = [.Alert, .Badge, .Sound]
    UIApplication.sharedApplication().registerForRemoteNotificationTypes(type)
}
```

* デバイストークンが取得されると、`didRegisterForRemoteNotificationsWithDeviceToken`メソッドが呼ばれます
* `didFinishLaunchingWithOptions`メソッドの下に下記のコードを書いてください

```swift:Swift3_AppDelegate.swift
// デバイストークンが取得されたら呼び出されるメソッド
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    // 端末情報を扱うNCMBInstallationのインスタンスを作成
    let installation : NCMBInstallation = NCMBInstallation.current()
    // デバイストークンの設定
    installation.setDeviceTokenFrom(deviceToken)
    // 端末情報をデータストアに登録
    installation.saveInBackground {error in
        if error != nil {
            // 端末情報の登録に失敗した時の処理
        } else {
            // 端末情報の登録に成功した時の処理
        }
    }
}
```

```swift:Swift2_AppDelegate.swift
// デバイストークンが取得されたら呼び出されるメソッド
func application(application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: NSData){
    // 端末情報を扱うNCMBInstallationのインスタンスを作成
    let installation = NCMBInstallation.currentInstallation()
    // デバイストークンの設定
    installation.setDeviceTokenFromData(deviceToken)
    // 端末情報をデータストアに登録
    installation.saveInBackgroundWithBlock { (error: NSError!) -> Void in
        if (error != nil){
            // 端末情報の登録に失敗した時の処理
        }else{
            // 端末情報の登録に成功した時の処理
        }
    }
}
```
### *アプリの再インストール時を考慮する場合の配信端末情報の登録*

* アプリが再インストールされた場合などに、前回のインストール時に保存された端末情報が残ったままになることが考えられます
* すると端末情報の登録でデバイストークンの__重複エラー__が発生するため、エラーが発生したときの処理を実装する必要があります
* 端末情報でエラーが発生した場合にエラーコードを取得し、__重複してはいけない値を登録しようとした場合に発生するエラーコード:409001__かどうかを判定し、データストアに保存されている端末情報を__上書き__するようにします

```swift:Swift3_AppDelegate.swift
// デバイストークンが取得されたら呼び出されるメソッド
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data){
    // 端末情報を扱うNCMBInstallationのインスタンスを作成
    let installation = NCMBInstallation.current()
    // デバイストークンの設定
    installation?.setDeviceTokenFrom(deviceToken)
    // 端末情報をデータストアに登録
    installation?.saveInBackground({ (error) in
        if error != nil {
            // 端末情報の登録に失敗した時の処理
            let err = error as! NSError
            if (err.code == 409001){
                // 失敗した原因がデバイストークンの重複だった場合
                // 端末情報を上書き保存する
                self.updateExistInstallation(currentInstallation: installation!)
            }else{
                // デバイストークンの重複以外のエラーが返ってきた場合
            }
        } else {
            // 端末情報の登録に成功した時の処理
        }
    })
    }

// 端末情報を上書き保存するupdateExistInstallationメソッドを用意
func updateExistInstallation(currentInstallation : NCMBInstallation){
    let installationQuery = NCMBInstallation.query()
    installationQuery?.whereKey("deviceToken", equalTo:currentInstallation.deviceToken)
    do {
        let searchDevice = try installationQuery?.getFirstObject() as! NCMBInstallation
        // 端末情報の検索に成功した場合
        // 上書き保存する
        currentInstallation.objectId = searchDevice.objectId
        currentInstallation.saveInBackground({ (error) in
            if (error != nil){
                // 端末情報の登録に失敗した時の処理
            }else{
                // 端末情報の登録に成功した時の処理
            }
        })
    } catch let searchErr as NSError {
        // 端末情報の検索に失敗した場合の処理
    }
}
```

```swift:Swift2_AppDelegate.swift
// デバイストークンが取得されたら呼び出されるメソッド
func application(application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: NSData){
    // 端末情報を扱うNCMBInstallationのインスタンスを作成
    let installation = NCMBInstallation.currentInstallation()
    // デバイストークンの設定
    installation.setDeviceTokenFromData(deviceToken)
    // 端末情報をデータストアに登録
    installation.saveInBackgroundWithBlock { (error: NSError!) -> Void in
        if (error != nil){
            // 端末情報の登録に失敗した時の処理
            if (error.code == 409001){
                // 失敗した原因がデバイストークンの重複だった場合
                // 端末情報を上書き保存する
                self.updateExistInstallation(installation)
            }else{
                // デバイストークンの重複以外のエラーが返ってきた場合
            }
        }else{
            // 端末情報の登録に成功した時の処理
        }
    }
}

// 端末情報を上書き保存するupdateExistInstallationメソッドを用意
func updateExistInstallation(currentInstallation : NCMBInstallation){
    let installationQuery = NCMBInstallation.query()
    installationQuery.whereKey("deviceToken", equalTo:currentInstallation.deviceToken)
        do {
            let searchDevice = try installationQuery.getFirstObject()
            // 端末情報の検索に成功した場合
            // 上書き保存する
            currentInstallation.objectId = searchDevice.objectId
            currentInstallation.saveInBackgroundWithBlock { (error: NSError!) -> Void in
                if (error != nil){
                    // 端末情報の登録に失敗した時の処理
                }else{
                    // 端末情報の登録に成功した時の処理
                }
            }
        } catch let searchErr as NSError {
            // 端末情報の検索に失敗した場合の処理
        }
    }
}
```

## 4. 開発者用証明書とプロビショニングプロファイルを設定して、実機デバッグビルドを行う
XcodeのSwiftアプリに開発用証明書とプロビショニングプロファイルの設定をします

* プロジェクトを選択 ＞「Build Settings」を選択 ＞「Code Signing」を表示します
 * 「Code Signing Identity」で「Automatic」を選択します
 * 「Provisioning Profile」で作成したプロビショニングプロファイルを選択します
![097.png](https://qiita-image-store.s3.amazonaws.com/0/112032/61948e17-8c00-6e39-1f32-4c4e1678180c.png)

* さらに、プロジェクトを選択 ＞「General」を選択 ＞「Identity」を表示します
 * 「Bundle Identifier」にAppID作成時に入力したBundleIDが入力されていることを確認します（違ったら修正します）
![098.png](https://qiita-image-store.s3.amazonaws.com/0/112032/a57439c1-b2bf-f280-f3df-f0a1706920f9.png)

* iPhoneをMacに接続して、左上のDevice選択欄から接続したiPhoneを選択します
![099.png](https://qiita-image-store.s3.amazonaws.com/0/112032/9254a8d8-6452-33c9-71c6-d4851ac979d5.png)

* 実行ボタンをクリック！！
* 実機でビルドに成功できたら（※iPhoneでアプリを起動し、プッシュ通知の許可までを行ってください）、[ニフティクラウドmobile backend](http://mb.cloud.nifty.com/)ダッシュボード上を確認します
* データストアのinstallationクラスにデバイストークンが登録されたことを確認します
![図F13.png](https://qiita-image-store.s3.amazonaws.com/0/112032/315d6d05-4f69-e55e-66ba-0371021462c0.png)

## 5. 動作確認！プッシュ通知を送りましょう★
* [ニフティクラウドmobile backend](http://mb.cloud.nifty.com/)ダッシュボード「プッシュ通知」＞「プッシュ通知」をクリックすると、一覧画面がでます
* 右上の「＋新しいプッシュ通知」をクリックするとここからプッシュ通知を作成できます
![図F15.png](https://qiita-image-store.s3.amazonaws.com/0/112032/93b5d46d-3d0d-51b4-a61e-b7f99f8a1874.png)

* 操作は以上です！ちゃんと届きましたか～？？
![096.png](https://qiita-image-store.s3.amazonaws.com/0/112032/1d44f002-143f-4764-c474-187be6c85e47.png)
（上記画像はXcode7のものです）

# 参考
* [ ニフティクラウドmobile backendのiOS(Objectiv-C SDK)用ドキュメント（プッシュ通知）](http://mb.cloud.nifty.com/doc/current/push/basic_usage_ios.html)