EY-Office ブログ

自作Todoアプリに機能追加をしようとしてSilent Push Notificationを知る

以前自分専用Todoアプリをリニューアルしたに書いたTodoアプリですが2年近く使っていましたが、最近問題点が出てきたので改造する事にしました。

問題点は、Todoアプリを起動する習慣がうすれ大事なタスクを忘れてしまう事故が発生するようになった事です。このアプリはバックエンドにRedmineを使っていてMacではブラウザーからタスクを追加・変更しています。またAWS Lambdaを使い定期的なタスクは自動的に登録されます。
これらのタスクをTodoアプリ(iPhone/iPad)で見落としてしまう事があるのです。

Silent

そうだバッチを表示しよう

iPadやiPhoneを使わない日はないのですが、Todoアプリを開く習慣が無くなりました(作った直後は毎日開いていましたが、2年近く経つとアプリそのものはへの関心は下がります😅)。そこで思いついたのは高プライオリティのタスク数をバッチで表示することです。

私はiOSのバッチは嫌いで最低限のアプリでしか表示していませんが、メールやSlackのような通信系はバッチを表示しています、同様にTodoアプリにもバッチを表示すればきっとアプリを開くはずです。

タスク情報の自動取得

しかし、バッチを表示するための情報取得はどうしましょうか?現在のアプリは起動時かテーブルを下に引っ張り更新した時のみです。
このままでは、Macや自動登録された高プライオリティのタスクの情報はiPad/iPhoneには反映されません。何か上手い方法がないでしょうか?Unixであればcron等でコードを動かせますが・・・

Background Fetch

調べてみると、Background Fetch(Using Background Tasks)を使うと、アプリ起動前にバックグラウンドで情報を取得できるようです。ただし情報取得のタイミングはiOS任せないようです(未確認)。

Silent Push Notification

そこで思い出したのはPush Notificationです。サーバー側にもコードを準備しなといけませんが、これはAWS Lambdaで簡単に実装できます。
久しぶりにPush Notificationを使うのでネットを調べてみるとSilent Push Notification(Pushing Background Updates)というものがあることを知りました(iOSのコードを書くのは久しぶりなので)。

通常のPush Notificationはメッセージやバッチが表示されますが、Silent Push Notificationはこのような表示はありませんが、バックグラウンド動作中のアプリに情報を伝える事ができます。

Todoアプリに組み込んでみた

Redmineの動いているサーバーからPush Notificationを送ればバッチに表示する高プライオリティのタスク数を後れますが、今回はシンプルにAWS Lambdaから午前6時にPush Notificationを送り、iPad/iPhoneのPush Notification受信メソッド内でRedmineから最新情報を取得し、必要に応じバッチを表示する事にしました。

1. Push Notificationといえば証明書

Push Notificationを送るコードはライブラリー等を使えば兆簡単ですが、Push Notificationを使うためには証明書や対応したプロビショニングが必要ななります。この手順が兆面倒ですが😅、現在ではネット上に丁寧な情報があるので助かりました。

2. iOS受信側

AppDelegate.swiftに以下のメソッドを追加しました。

  • ① Push Notification許可表示の
  • ② Push Notification許可結果の受け取り
  • ③ Push Notification情報の取得
    • この中でRedmineからの情報と高プライオリティタスク数取得メソッドProjects.getHighIssuesCountを呼び出し
    • 上が成功したらUIApplication.shared.applicationIconBadgeNumber =でバッチを表示
    • サーバーにデータ.newData受信成功とデータ処理成功ステータスを戻しています
// ①
func application(_ application: UIApplication, didFinishLaunchingWithOptions
    launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge])
        { granted, _ in
            print("Permission granted: \(granted)")
            guard granted else { return }
            DispatchQueue.main.async {
                UIApplication.shared.registerForRemoteNotifications()
            }
        }
        return true
    }

//  ②
func application(_ application: UIApplication,
    didFailToRegisterForRemoteNotificationsWithError error: Error) {
    print("Failed to register to APNs: \(error)")
}

// ③
func application(_ application: UIApplication, didReceiveRemoteNotification 
    userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: 
    @escaping (UIBackgroundFetchResult) -> Void) {
    Projects.getHighIssuesCount(done: {count in
        UIApplication.shared.applicationIconBadgeNumber = count
        completionHandler(.newData)
    })
}

3. 送信側AWS Lambda

  • ① Push Notificationライブラリーは @parse/node-apn を使いました。
    • 有名なnode-apnは現在メンテされてないようなので、このフォークを使いました
    • また、node-apnはLambdaでタイムアウトすることがあります、shutdwonで接続が解除されないようです
  • ② 開発環境ではdotenvを使い環境変数を設定しています。AWS LambdaではLambdaの環境変数を使います
  • ③ 秘密キーは複数行です。dotenvは複数行に対応していますが、Lambdaは対応できててないのでreplaceで対応しています
  • ④ Apple Push Notification serverとの接続を作成
  • ⑤ Notificationデータの作成、Silent Push NotificationではcontentAvailable: true, priority: 5, pushType: 'background'を指定します
  • ⑥ Notificationの送信、複数のiOSデバイスIDは配列で指定できます
  • ⑦ Apple Push Notification serverとの接続を解除
  • ⑧ AWS Lambdaのエントリーポイント
  • ⑨ 開発環境(Mac)での起動コード

LambdaはAmazon EventBridgeを使い毎日6時に実行しています。また、[Amazon API Gateway ](Amazon API Gateway)で秘密のURLを割り当てURLアクセスでも実行できるように設定しました。

const apn = require('@parse/node-apn')   // ①
require('dotenv').config()               // ②

const sendNotifications = async (deviceTokens) => {
  const tokenKey = process.env["TOKEN_KEY"].replace(/\\n/g, '\n')  // ③

  const apnProvider = new apn.Provider({  // ④
    token: {
      key: tokenKey,
      keyId: process.env["TOKEN_KEYID"],
      teamId: process.env["TOKEN_TEAMID"]
    },
    production: false
  })

  const notification = new apn.Notification({  // ⑤
    topic: process.env["NOTIFICATION_TOPIC"],
    contentAvailable: true,
    priority: 5,
    pushType: 'background'
  })

  try {
    const result = await apnProvider.send(notification, deviceTokens)  // ⑥
    console.log("-- ok "); console.dir(result)
    apnProvider.shutdown()  // ⑦
  } catch (err) {
    console.log("-- error "); console.dir(err)
    apnProvider.shutdown()  // ⑦
  }
}

exports.handler = async (_event, _context, callback) =>  {    // ⑧
  await sendNotifications(process.env["DEVICE_TOKENS"].split(","))
  callback()
}

if (process.platform == "darwin") {  // ⑨
  exports.handler({}, {}, () => {
    console.log("end")
  })
}

まとめ

わずかなコードで希望の機能が実現できました。Push Notificationはサーバー側からアプリをコントロールできるのは素晴らしいですね。 アプリが終了している時は使えないようですが何か方法はないのでしょうか?

- about -

EY-Office代表取締役
・プログラマー
吉田裕美の
開発者向けブログ