[swift]挫折しながら覚えるiOS開発その11 ローカルプッシュ通知(UILocalNotification)の追加


[まとめ] 現在開催中のKindleセール情報はこちら

前回は、UIRefreshControlを使って引っ張ってリロードを行う機能を追加しました。

今回はローカルプッシュ機能を追加したいと思います。

プッシュ通知には、ローカルプッシュとリモートプッシュの2種類がありますが、週刊Qiitaの場合は毎週水曜の12時にAPI更新をお知らせするプッシュ通知を飛ばすのみなのでローカルプッシュで対応することにしました。

ローカルプッシュの実装に関しては以下の参考書籍で学びました。

参考書に倣ってAppDelegate.swiftにローカルプッシュの設定を書いていきます。

1. ローカルプッシュ通知の基本設定

まずは、application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?)メソッドにregisterUserNotificationSettingsを追加して、アプリ初回起動時にプッシュ通知の許可をユーザに求める設定を行います。

以前はローカルプッシュの場合はユーザ許可が必要なかったのですが、最近ではローカルプッシュも許可が必要になったそうです。

そして、applicationDidEnterBackgroundメソッド内ではアプリがバックグランドになった時に、ローカルプッシュを設定するようにしています。

まずは試しに、バックグランドになって10秒後にプッシュ通知をするようにしてみます(notification.fireDate = NSDate(timeIntervalSinceNow: 10)の部分)。

# AppDelegate.swift

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        // Override point for customization after application launch.

        // ユーザのpush通知許可をもらうための設定
        application.registerUserNotificationSettings(
            UIUserNotificationSettings(forTypes:
                UIUserNotificationType.Sound
                | UIUserNotificationType.Badge
                | UIUserNotificationType.Alert, categories: nil)
        )

        return true
    }

    func applicationWillResignActive(application: UIApplication) {
        // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
        // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
    }

    func applicationDidEnterBackground(application: UIApplication) {
        // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
        // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.

        // push設定
        // 登録済みのスケジュールをすべてリセット
        application.cancelAllLocalNotifications()

        var notification = UILocalNotification()
        notification.alertAction = "アプリに戻る"
        notification.alertBody = "ランキングが更新されました"
        notification.fireDate = NSDate(timeIntervalSinceNow: 10)  // Test
        notification.soundName = UILocalNotificationDefaultSoundName
        // アイコンバッジに1を表示
        notification.applicationIconBadgeNumber = 1
        // あとのためにIdを割り振っておく
        notification.userInfo = ["notifyId": "ranking_update"]
        application.scheduleLocalNotification(notification)
    }

    func applicationWillEnterForeground(application: UIApplication) {
        // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
    }

    func applicationDidBecomeActive(application: UIApplication) {
        // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
    }

    func applicationWillTerminate(application: UIApplication) {
        // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
    }
}

また、現状だとアプリ名が「weeqiita」になっているので、通知受け取り時の表示が「週刊Qiita」になるように調整します。

アプリ設定のInfoを開き、「Bundle display name」の項目を追加し、valueに「週刊Qiita」を設定します。

swift11-3

この状態でシミュレータを起動すると、通知許可ダイアログが表示されます。

OKを押してから、ホーム画面に戻る(バックグランドに移動する)と10秒後にプッシュ通知が来ます。

swift11-1

2. 通知経由でアプリを起動した際の設定

1.でとりあえずローカルプッシュ通知を行うことはできたのですが、今は通知経由でアプリを開いても特に何もしない状態になっています。

前回、setupLinksメソッドにキャッシュを行う設定を追加しました。

そのため、「ランキングが更新されました」という通知が来ても、キャッシュが残っている場合はデータが更新されない現象が起きてしまいます。

そこで、プッシュ通知経由でアプリを起動した場合は、強制的にAPIを再読み込みするように設定したいです。

プッシュ通知経由でアプリを起動するタイミングとしては、バックグランド状態とアプリ終了状態の2通りがあり、それぞれのタイミングで呼ばれるメソッドは以下になります。

状態 該当メソッド
バックグランド状態 application(application: UIApplication, didReceiveLocalNotification notification: UILocalNotification)
アプリ終了状態 application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?)

それぞれのコードの実装は以下のようにしました。

# AppDelegate.swift

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    // Override point for customization after application launch.

    // push設定
    application.registerUserNotificationSettings(
        UIUserNotificationSettings(forTypes:
            UIUserNotificationType.Sound
            | UIUserNotificationType.Badge
            | UIUserNotificationType.Alert, categories: nil)
    )

    // アプリを終了していた際に、通知からの復帰をチェック
    if let notification = launchOptions?[UIApplicationLaunchOptionsLocalNotificationKey] as? UILocalNotification {
        localPushRecieve(application, notification: notification)
    }
    // バッジをリセット
    application.applicationIconBadgeNumber = 0

    return true
}

func application(application: UIApplication, didReceiveLocalNotification notification: UILocalNotification) {
    // アプリがActiveな状態で通知を発生させた場合にも呼ばれるのでActiveでない場合のみ実行するように
    if application.applicationState != .Active {
        localPushRecieve(application, notification: notification)
    }
}

func localPushRecieve(application: UIApplication, notification: UILocalNotification) {
    if let userInfo = notification.userInfo {
        switch userInfo["notifyId"] as? String {
        case .Some("ranking_update"):
            reloadTableFromPush()
            break
        default:
            break
        }
        // バッジをリセット
        application.applicationIconBadgeNumber = 0
        // 通知領域からこの通知を削除
        application.cancelLocalNotification(notification)
    }
}

func reloadTableFromPush() {
    let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
    let masterTableViewController = mainStoryboard.instantiateViewControllerWithIdentifier("MasterTableViewController") as! MasterTableViewController

    masterTableViewController.setupLinks(forceReload: true)
}

通知からの起動のタイミングでlocalPushRecieveを実行し、localPushRecieve内ではプッシュ通知IDに応じて処理を分けるようにしています。

reloadTableFromPushでは、masterTableViewControllerのsetupLinksに引数を渡して強制的にAPIを再読み込みするようにしています。

これでプッシュ通知経由で起動した際にAPIの更新ができるようになりました。

3. 通知のタイミングを設定

最後にプッシュ通知の通知タイミングが仮状態のままになっているので、「水曜日の12:00:00」に通知が飛ぶようにnotification.fireDateの設定をします。

アプリをバックグランドに入れたタイミングで「次の水曜日」を取得できるようにしたいので、現在時刻から次の水曜を探すメソッドを追加しました。

曜日を数値で書いておくと後で分かりづらいので、曜日を表すenumを追加しました。(week.swift)

# week.swift

enum Week :Int {
    case Sunday = 1     // 日曜日
    case Monday = 2     // 月曜日
    case Tuesday = 3    // 火曜日
    case Wednesday = 4  // 水曜日
    case Thursday = 5   // 木曜日
    case Friday = 6     // 金曜日
    case Saturday = 7   // 土曜日
}

そして、util.swiftを追加し、クラスメソッドとしてnextFireDateを追加しました。

NSDateとNSCalendarを組み合わせて次の水曜12時を取得するようにしています。

# util.swift

import Foundation

class Util {
    // 次の水曜12時を返す
    class func nextFireDate() -> NSDate {
        var date = NSDate()
        var calender = NSCalendar.currentCalendar()
        var components = calender.components(
            .CalendarUnitYear |
            .CalendarUnitMonth |
            .CalendarUnitDay |
            .CalendarUnitWeekday, fromDate: date)
        var weekday = components.weekday  // 1が日曜
        var hour = components.hour

        var fireWeekday = Week.Wednesday.rawValue
        var interval: NSTimeInterval
        if (weekday >= fireWeekday && hour >= 12) {
            interval = Double(60 * 60 * 24 * ((7 + fireWeekday) - weekday))
        } else {
            interval = Double(60 * 60 * 24 * (fireWeekday - weekday))
        }

        var nextDate = date.dateByAddingTimeInterval(interval)
        var fireDateComponents = calender.components(
            .CalendarUnitYear |
            .CalendarUnitMonth |
            .CalendarUnitDay |
            .CalendarUnitWeekday, fromDate: nextDate)
        fireDateComponents.hour = 12
        fireDateComponents.minute = 0
        fireDateComponents.second = 0

        return calender.dateFromComponents(fireDateComponents)!
    }
}

最後にAppDelegateのnotification.fireDateにnextFireDateを設定します。

# AppDelegate.swift

notification.fireDate = Util.nextFireDate()

これで次の水曜12時になったらプッシュ通知が届くようになりました。

4. プッシュ通知のテスト

プッシュ通知のテストは実機の時計をずらして確認します。

「設定」->「一般」->「日付と時刻」->「自動設定」をOFFにします。

あとは日付を調整すると水曜12時になったタイミングで通知が来ます。

これを、アプリを終了している場合と、バックグランドにいる状態でそれぞれテストします。

swift11-2

これでローカルプッシュ通知が実装できました。

ローカルプッシュ設定には繰り返しの通知設定もできるのですが、アプリを使わなくなっても毎週通知が来るのはユーザの迷惑になってしまうので、アプリを起動して閉じた際に次の水曜に通知を1回分だけ設定するという仕様にしました。

こうすることでアプリを定期的に起動してくれるユーザには定期的に通知が飛び、アプリを起動していないユーザには通知が飛ばなくなるという機能が実現できます。

次回はiAdを組み込んでみます。

参考

[まとめ] 現在開催中のKindleセール情報はこちら