サーバーを持たない iPhone アプリの作り方——Sheet Widget のデータの置き場所
Sheet Widget は、Google スプレッドシートの内容を iPhone のウィジェットに表示するアプリです。このアプリは、開発者のサーバーを一切持っていません。シートの内容も、Google のログイン情報も、開発者のところには届きません。
家計簿や売上の数字のように、人に見せたくないデータを扱うアプリなので、「データを預からない」ことを最初から前提にしました。サーバーがなくても、ログイン・ウィジェットの更新・端末間の同期・変更の通知まで作れたので、どこに何を置いたかをまとめます。
全体の構成
Google(Sheets API・Drive API)
↑ 直接通信(開発者のサーバーは経由しない)
iPhone
├─ アプリ本体:Google にログイン、ウィジェットの設定を作る
├─ キーチェーン:Google のログイン情報
├─ App Group:ウィジェットの設定、取得したシートの内容、画像
└─ ウィジェット:App Group を読んで表示、自分でも Google に取りに行く
↓(任意)
利用者自身の iCloud:ウィジェットの設定だけを同期
ログイン情報:キーチェーン(バックアップにも入れない)
Google のログイン情報(リフレッシュトークン)は、iPhone のキーチェーンに保存しています。キーチェーンは、パスワードなどを暗号化して保存する、iOS の仕組みです。
保存するときは、アクセスできる条件として kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly を指定しています。
AfterFirstUnlock:起動後に一度ロックを解除すれば、画面がロック中でも読める。ウィジェットはロック中にも更新されるので、この条件が必要でした。ThisDeviceOnly:この端末だけ。iCloud バックアップや別の端末への移行には含まれません。
最初は、ログイン情報を App Group(次の項目)の設定ファイルに置いていました。App Group は、同じ開発者のアプリと拡張機能しか読めない場所ですが、中身は共有フォルダの中の設定ファイルなので、暗号化していない端末のバックアップにそのまま含まれてしまいます。リフレッシュトークンは長い期間使えるので、バックアップが漏れると、選んだシートを読まれ続けるおそれがあります。そこで、1.0 の審査の途中で出し直したビルドで、キーチェーンに移しました。すでに使っている人の端末では、最初に読み込んだときに自動で移し替えるので、ログインし直す必要はありません。
もう1つの注意点は、アプリ本体とウィジェットの拡張機能で、キーチェーンを共有する設定が必要なことです。両者はバンドル ID が違うので、標準のままでは互いに保存したものを読めません。両方の設定(entitlements)に同じ keychain-access-groups を書き、保存するときにもそのグループを明示しています。機種変更のときは、新しい端末でログインし直してもらう前提にしています。
ウィジェットとの受け渡し:App Group
iPhone のウィジェットは、アプリ本体とは別のプログラム(拡張機能)として動きます。アプリ本体と拡張機能でデータを受け渡すには、App Group という共有の保存場所を使います。
Sheet Widget では、App Group に次のものを置いています。
- ウィジェットの設定(どのシートの、どの範囲を、どのサイズで表示するか)
- 取得したシートの内容(セルの値と書式)
=IMAGE()関数で入れた画像(縮小して保存したもの)
ウィジェットは、表示する瞬間にネットから画像を取りに行くことができません。そのため、データを取得するタイミングで画像もまとめてダウンロードし、長い辺を 480px までに縮小して App Group に保存しておき、表示するときはそれを読むだけにしています。
ウィジェット自身が Google に取りに行く
ウィジェットの更新のたびにアプリを開いてもらうのは不便なので、ウィジェットの拡張機能が自分で Google のAPI を呼んで、最新の内容を取得しています。アプリ本体と同じログイン情報を使えるように、キーチェーンと App Group を共有しています。
iOS がウィジェットを更新する間隔は、おおむね15〜60分です(いつ更新するかは iOS が決めます)。すぐに最新にしたいときのために、ウィジェットに更新ボタンも置いています(Pro)。
変更の通知も、端末の中で
Sheet Widget(Pro)には、表示している範囲の値や書式が変わったら通知する機能があります。これもサーバーからの通知(プッシュ通知)ではありません。ウィジェットが新しい内容を取得したときに、前回の内容と端末の中で比べて、変わっていたら端末の中から通知を出しています。
その代わり、通知が届くのは iOS がウィジェットを更新したタイミングになるので、シートを変えてから通知が来るまでには時間差があります。サーバーを持たないことと引き換えの制約として、アプリの説明にも書いています。
端末間の同期:利用者自身の iCloud(オプトイン)
iPhone を買い替えたときや、複数の端末で使うときのために、ウィジェットの設定だけを iCloud で同期できるようにしました。
- 使うのは
NSUbiquitousKeyValueStore(iCloud のキー・バリュー保存)。保存先は利用者自身の iCloud で、開発者はアクセスできません。 - 同期するのは設定だけ。ログイン情報とシートの内容は同期しません。
- 既定はオフ。オンにした人だけが使います。
設定は数KBの JSON なので、キー・バリュー保存の容量に十分収まります。
サーバーを持たないことの代償
サーバーがないと、できないことや手間が増えることもあります。
- 一斉のお知らせを送れない。 プッシュ通知で全員に告知する、といったことはできません。
- 利用状況が分からない。 アクセス解析の仕組みを入れていないので、どの機能がよく使われているかは、App Store の数字や問い合わせから推測するしかありません。
- 更新のタイミングは iOS 任せ。 数分おきに確実に更新する、といったことはできません。
それでも、「シートの内容も、ログイン情報も、開発者に届かない」と言い切れることは、このアプリにとって何より大事な価値だと考えています。広告もトラッキングも入れていません。
Sheet Widget は App Store で公開しています。