iOSアプリに強制バージョンアップ機能を入れよう!(Firebase Realtime Database編)

ZAICOでは、Android・iOS・Rubyエンジニアを絶賛募集中です! 詳しくは、採用ページをご覧ください。

好きな場所で働こう

こんにちは!ZAICOの宴会副部長兼エンジニアのいくえもんです。先日、オミクロン株爆発前に部長の計らいで初めてのオフ会が開催され、まだその余韻に浸っています。

泥酔して記憶がなくなる前に社内ハッカソン(FedEx Week)の内容をブログ化します!FedEx Weekではそれぞれ自由にテーマを決めて、普段の仕事とは違うことに挑戦します。私の今回のテーマはFirebaseを使い倒す!です。

先日モバイルアプリ内で画像付きお知らせを表示しよう!でFirebase In-App Messagingについてブログを書きましたが、今回はその第二弾として、Firebase Realtime Databaseを使った強制バージョンアップ機能の作成をご紹介します。

やりたかったこと

アプリ開発をしていると、バックエンドの仕様変更や重大なバグの修正などで、アプリバージョンのサポート下限を限定した時がありますよね?そんな時にはこの画像のようにバージョンアップのお知らせを表示し、強制的にアプリの更新を促すことができます。

Androidアプリでアプリ内アップデートを表示するでご紹介したようにAndroidではGoogle Play Core Libraryを使って実装することができますが、iOSにはAppleから提供されている標準の強制バージョンアップ機能はありません。このため、下の仕様に基づき自主制作しました。

  1. アプリのバージョンが下限バージョン未満の場合アラートを表示する
  2. アラートにはApp Storeへのリンクをつける。リンクをタップするとApp Storeアプリが起動し、アプリダウンロードページが表示される。
  3. アラートはアプリバージョンが条件を満たすまで消すことができない
  4. アラート表示中はアプリ内の操作が一切できない
  5. 下限バージョンとApp StoreのURLはアプリ外でいつでも変更できる(アプリリリース不要)
  6. ユーザーがアプリ利用中でも下限バージョンが変更されたら条件判定を実行し、下限バージョンの条件を満たさなければ即座にアラートを表示する
  7. ユーザーがアプリを利用していない時に下限バージョン条件を満たさなくなった場合には、アプリ起動時(またはForegroundになった時)に即座にアラートを表示する

ちなみに、この強制バージョンアップ機能はアプリの初回リリース時に実装することをお勧めします。例えば、バージョン2.0でこの機能を実装したら、バージョン1.xのユーザーさん達を強制的にバージョンアップさせることができなくなります!新規アプリを開発検討中の方は忘れずにこの機能を必須要件にしましょう!!

方針

強制バージョンアップ機能の作成方法には様々あります。例としては、下みたいな感じです。

  • APIのリクエストヘッダにアプリバージョン情報を入れて送信し、サーバーでバージョン判定してエラーを返す→APIリクエストの度にバージョンチェックする
  • バージョン取得用のAPIを作成し、アプリ起動時(またはForegroundになった時)にAPIを呼び出す→アプリ起動時のみバージョンチェックする

もちろんこれら方法でも強制バージョンアップには十分な気もしますが、「仕様#6: ユーザーがアプリ利用中でも下限バージョンが変更されたら条件判定を実行し、下限バージョンの条件を満たさなければ即座にアラートを表示する」を実現するには不十分でした。

そこで、こんな時に頼りにするのはもちろんGoogle先生です。

Google先生が提供するFirebaseにはRealtime DatabaseというNoSQL型のデータベースがあります。詳しい&最新の情報は本家HPを見ていただければと思いますが、次のような特徴があります。

  • リアルタイム:データが更新されるとDBに接続しているクライアントは即座に通知を受け取れます
  • オフラインサポート:オフライン時にはクライアントローカルに保存されたデータを利用します。オンラインになったら自動的に最新データを取得し、データに変更があればクライアントに通知してくれます。超便利!神!!
  • 有料!!??:大概のものが無料で使えるFirebaseですが、無料プランだと制限があります。一番厳しい条件が「同時接続数100まで」です。同時接続数とは、DBに接続しているセッション数なので、総ユーザー数とは異なりますが、ある程度の規模のアプリであれば100ユーザーが同時利用することは珍しくないので課金が必要です。ちなみに、課金すると同時接続数の上限は20万になります。仕様#6が必要なければ無料で実装できる他の方法を検討するのも手です。

今回は仕様#6があるので、Realtime Databaseを使ってみましょう!

実装

1. Firebase Realtime Databaseを設定する

何はともあれDBがないと始まらないのでDBを作りましょう。詳しいDBの作り方は本家HPを参照してください。

1.1 Firebase Realtime Databaseのルール

Realtime Databaseは接続先URLを知っていれば誰でも接続することができます。このため、誰がどのデータにアクセス可能かをいう権限をJSONを利用して設定していきます。今回はRealtime Databaseにバージョン情報のみ保存するので、DB全体を「読み取り:誰でも可、書き込み:不可」という設定にしました。

{
  "rules": {
    ".read": true,
    ".write": false
  }
}

DBにユーザーに属するデータなども保存する場合はノードごとに詳細な権限設定をするのを忘れないようにしましょう!

1.2 Firebase Realtime Databaseのデータ

Firebase Realtime DatabaseはJSON形式でデータを保存しますが、Firebase Consoleを利用するとインタラクティブに設定することも可能です。今回は下のようにversionノード下にminVersionstoreUrlをString型で作成しました。

{
  "version" : {
    "minVersion" : "1.0.0",
    "storeUrl" : "https://itunes.apple.com/jp/app/<YourAppStoreURL>"
  }
}

こちらをFirebase Consoleで見るとこのようになります。

今回は本番用のDBとテスト用のDBを2つ作成しました。本番環境でテストするわけにはいかないので、本番アプリのみ本番環境DBに接続し、それ以外のアプリは全てテスト用DBに接続します。上の画像はテスト用DBですが、本番用DBのノード構成も全く同じです。

また、赤枠で囲われた部分にDBの接続先URLが記載されています。このURLはDBごとに自動的に発行されるので、コピーしてアプリから利用してください。

2. Firebase Realtime Databaseに接続する

2.1 Firebase Realtime Database SDKを追加する

まずはRealtime DatabaseのSDKをプロジェクトに追加します。

本家HPではSwift Package Managerの方法を紹介していますが、弊社はCocoaPodsを利用しているため、以下をPodファイルに追加し、pod installコマンドを実行しました。

pod 'Firebase/Database'

2.2 Firebase Realtime DatabaseのversionノードをObserveする

タイトルが英語ばっかりになってきました。ルー大柴化が止まりません。

Firebase Realtime Databaseでは任意のノードをObserveすることができます。ノードをObserveすると、Observe開始直後と、そのノード又は子ノードの1つ以上が更新された場合に通知を受け取ることができます。今回はversionノードをObserveしますが、データベース全体をObserveし、あらゆるノードの更新を通知してもらうことも可能です。

versionノードをObserveすると、minVersion又はstoreUrlのどちらかが更新された場合や、version以下に子ノードが追加された場合に通知を受け取れます。

それでは、早速versionノードをObserveしましょう。

import FirebaseDatabase

class FirebaseDatabaseManager: NSObject {   
   func observeVersion() {
        // Realtime DBへのリファレンスを取得
        let db = Database.database(url: "Realtime DBのURL").reference()
        
        // すでにRealtime DBをObserveしている場合に備えて全てのObserverをキャンセル
        db.child("version").removeAllObservers()
        
        // versionノードをObserveする。Observeに成功した初回と、データが更新された場合にSnapshotが返却される。
        db.child("version").observe(.value) { (snapshot) in
            // DB更新時の処理はここに記載
        }     
    }
}

Realtime DBのURLは1.2章のコンソールに表示されているものを使います。実行環境に応じて正しいURLを取得してください。

そして、アプリ起動時又はアプリがForegroundに来たときにobserveVersionメソッドを呼び出します。AppDelegateに呼び出すためのコードを記載しましょう。

import Firebase

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {        
        // Firebaseのサービスを使うときは常に呼び出す
        FirebaseApp.configure()

        // アプリ起動時にDBをobserveする
        FirebaseDatabaseManager.observeVersion()

        return true
    }

    func applicationWillEnterForeground(_ application: UIApplication) {
        // アプリがForegroundになった時にDBをobserveする
        FirebaseDatabaseManager.observeVersion()
    }
}

これで以下の場合にアプリはRealtime Databaseから通知を受け取ることができます。

  • アプリ起動時
  • アプリがForegroundに来たとき
  • minVersionが更新された時
  • storeUrlが更新された時
  • versionに子ノードが追加された時

3. 強制バージョンアップのアラートを表示する

3.1 バージョンを比較する

弊社ではアプリのバージョンにx.y.z形式のSemantic Versioningを利用しています。しかしながら、何かのきっかけでyやzが省略されてしまった時などにも対応できるバージョン比較用の関数を作りました。

この関数はアプリのバージョンが引数のバージョン未満の場合にtrueを返します。

private func shouldUpdateVersion(minVersion: String) -> Bool {
    let appVersion = Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String // アプリバージョンを取得
    let appVersionArray = appVersion.components(separatedBy: ".") // アプリバージョンを.で区切って配列化
    let minVersionArray = minVersion.components(separatedBy: ".") // 下限バージョンを.で区切って配列化
    let iterationCount = max(appVersionArray.count, minVersionArray.count)
    
    // x.y.zのxから順に比較していく
    for index in (0...iterationCount-1) {
        // yやzが省略されている場合や数字以外の文字の場合は0とみなす。
        let appVersionNumber = appVersionArray.count > index ? Int(appVersionArray[index]) ?? 0 : 0
        let minVersionNumber = minVersionArray.count > index ? Int(minVersionArray[index]) ?? 0 : 0
        
        // 対象の桁の数字が同じなので次の桁をチェック
        if appVersionNumber == minVersionNumber {
            continue
        }
        // minVersionの方が大きければバージョンアップする
        return minVersionNumber > appVersionNumber
    }
    // バージョンが完全に一致したのでバージョンアップ不要
    return false
}

3.2 消せないAlertを表示する

次は強制バージョンアップアラートの表示です。このアラートは消すことができません、かつ、ボタンをタップするとApp Storeアプリを立ち上げます。

App Storeアプリは正しいURLさえ渡せば特別な処理は不要です。

また、複数のアラートが同時に出てくるとウザいので、すでにアラートが表示されている場合には追加のアラートは表示しないようにします。

private func showForceUpdateAlert(url: URL) {
    let alert = UIAlertController(
        title: "アラートのタイトル",
        message: "アラートの本文",
        preferredStyle: .alert
    )
    
    // App Storeを開くボタンを追加する
    alert.addAction(
        UIAlertAction(
            title: "ボタンのタイトル",
            style: .default,
            handler: { _ in
                  if UIApplication.shared.canOpenURL(url) {
                        UIApplication.shared.open(url)
                  }
            }
        )
    )
 
    // アラートを表示する   
    present(alert: alert)
}

private func present(alert: UIAlertController, on viewController: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) {
    
    // すでにアラートが表示されていたら何もしない
    guard viewController?.isKind(of: UIAlertController.self) == false else { return }

    // 一番上のViewを探す        
    if let presentedVC = viewController?.presentedViewController {
        present(alert: alert, on: presentedVC)
    } else {
        viewController?.present(alert, animated: true, completion: nil)
    }
}

3.3 Firebase Realtime Databaseが更新された際の処理を書く

それでは最後に上の関数を組み合わせてobserveVersionメソッドを完成させましょう。

func observeVersion() {
     // Realtime DBへのリファレンスを取得
     let db = Database.database(url: "DBのURL").reference()
     
     // すでにRealtime DBをObserveしている場合に備えてまずは全てのObserverをキャンセル
     db.child("version").removeAllObservers()
     
     // versionノードをObserveする。Observeに成功した初回と、データが更新された場合にSnapshotが返却される。
     db.child("version").observe(.value) { (snapshot) in
         // DB更新時の処理をここに書く
         // 必要情報(minVersionとstoreUrl)が取得できなければ何もしない
         guard let versionDict = snapshot.value as? [String: String], // DBからはDictionary型のsnapshotが返ってくる
               let minVersion = versionDict["minVersion"],
               let storeUrl = versionDict["storeUrl"],
               let url = URL(string: storeUrl) else { return }
         
         if shouldUpdateVersion(minVersion: minVersion) {
             // 閉じられないアラートを表示する
             showForceUpdateAlert(url: url)
         } else {
             // 強制バージョンアラートが不要になったら消す
             dismissForceUpdateAlert()
         }
     }
 }

最後に

ここまでお付き合いいただきありがとうございました。

今回はFirebase Realtime Databaseで強制バージョンアップを作りましたが、同様の仕組みでメンテナンスモードなども作成可能です。

また、ユーザーの能動的アクションがなくてもアプリ内の画面遷移をトリガーする(例えば、コンビニ決済アプリでQRコードをレジで読み取って決済完了すると完了画面に自動遷移する)など、Realtime Databaseの特性を活かして色々な機能を作ることができます。

さらに、Realtime Databaseと類似のサービスでより複雑なデータ型や検索をサポートするCloud Firestoreなどもあります。どちらのDBを使うべきかは本家HPをご覧ください。

興味を持たれた方はぜひRealtimeの世界で遊んでみてください!

ZAICOでは、新しいテクノロジーの力でモノの状態・流れを把握する仕組みに一緒に取り組む仲間を募集しております。
詳しくは、採用ページをご覧ください。

好きな場所で働こう