はじめに
この記事では、アプリを起動したとき、新バージョンがリリースされていることをユーザに伝え、アップデートを促す仕組み(この記事では半強制アップデートと呼ぶ) の実装方法を紹介します。
強制アップデートはゲームアプリなどでよく見る仕組みです。アップデートするまでアプリを利用できなくすることで、常に最新バージョンで動かすことができます。ただし、この方法はユーザがアップデートできる通信環境にいることを前提としているため、もし通信環境の悪いところにいた場合、ダウンロードに時間がかかったりして、すぐにアプリを使うことができなくなってしまいます。そういった使い勝手の観点から、僕はアップデートを促すまでに留めた半強制くらいが好みです。
さて、ユーザにインストールされているアプリのバージョンに乖離が生じると、最新機能を提供できないのはもちろんのこと、既存機能を改修しづらくなってきます。例えば、DBのマイグレーションを何世代かに渡って行っているとき、バージョンが飛んでいると思わぬバグを踏んだりします。
ユーザには常に最新バージョンを使ってもらうのが望ましく、アップデートを促す仕組みは積極的に取り入れた方が良いと考えています。
iTunes Search APIを利用した実装
半強制アップデートの仕組みを実装するとき、だいたい以下のフローが考えられます。
- アプリが起動されたらサーバに最新バージョンを問い合わせる
- サーバから返却されたバージョンと現バージョンを比較
- 最新ではなかったらユーザにバージョンアップを促すUIを表示
iTunes Search API
最新バージョンを返すサーバを自前で用意しても良いですが、ここではiTunes Search APIを利用します。このAPIを使うと、App Storeで配信されているアプリの情報を無料で取得することができます。アプリ情報にはApp Storeでリリースされているバージョン情報も含まれています。
iTunes Search APIを使うことで、アプリが実際にApp Storeから配信されるようになった段階で、取得できるアプリのバージョン情報が自動的に最新のものになるため、自前で配信バージョンを管理するサーバを立てる必要がなく、最新バージョンの更新し忘れなども起きないため、運用が楽です。
AppStoreクラス - APIを利用する
僕は以下のようなiTunes Search APIを叩くAppStoreクラスを実装しています。(通信ライブラリにAlamofire v5を使用しています)
import Foundation
import Alamofire
typealias LookUpResult = [String: Any]
enum AppStoreError: Error {
case networkError
case invalidResponseData
}
class AppStore {
private static let lastCheckVersionDateKey = "\(Bundle.main.bundleIdentifier!).lastCheckVersionDateKey"
static func checkVersion(completion: @escaping (_ isOlder: Bool) -> Void) {
let lastDate = UserDefaults.standard.integer(forKey: lastCheckVersionDateKey)
let now = currentDate
// 日付が変わるまでスキップ
guard lastDate < now else { return }
UserDefaults.standard.set(now, forKey: lastCheckVersionDateKey)
lookUp { (result: Result<LookUpResult, AppStoreError>) in
do {
let lookUpResult = try result.get()
if let storeVersion = lookUpResult["version"] as? String {
let storeVerInt = versionToInt(storeVersion)
let currentVerInt = versionToInt(Bundle.version)
completion(storeVerInt > currentVerInt)
}
}
catch {
completion(false)
}
}
}
static func versionToInt(_ ver: String) -> Int {
let arr = ver.split(separator: ".").map { Int($0) ?? 0 }
switch arr.count {
case 3:
return arr[0] * 1000 * 1000 + arr[1] * 1000 + arr[2]
case 2:
return arr[0] * 1000 * 1000 + arr[1] * 1000
case 1:
return arr[0] * 1000 * 1000
default:
assertionFailure("Illegal version string.")
return 0
}
}
/// App Storeを開く
static func open() {
if let url = URL(string: storeURLString), UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url)
}
}
}
private extension AppStore {
static var iTunesID: String {
"<YOUR_ITUNES_ID>"
}
/// App Storeのアプリページ
static var storeURLString: String {
"https://apps.apple.com/jp/app/XXXXXXX/id" + iTunesID
}
/// iTunes Search API
static var lookUpURLString: String {
"https://itunes.apple.com/lookup?id=" + iTunesID
}
/// 現在日時から生成される20201116のような整数を返す
static var currentDate: Int {
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .gregorian)
formatter.locale = .current
formatter.dateFormat = "yyyyMMdd"
return Int(formatter.string(from: Date()))!
}
static func lookUp(completion: @escaping (Result<LookUpResult, AppStoreError>) -> Void) {
AF.request(lookUpURLString).responseJSON(queue: .main, options: .allowFragments) { (response: AFDataResponse<Any>) in
let result: Result<LookUpResult, AppStoreError>
if let error = response.error {
result = .failure(.networkError)
}
else {
if let value = response.value as? [String: Any],
let results = value["results"] as? [LookUpResult],
let obj = results.first {
result = .success(obj)
}
else {
result = .failure(.invalidResponseData)
}
}
completion(result)
}
}
}
extension Bundle {
/// Info.plistにあるバージョン番号を取得。major.minor.patch形式になっていることを前提とする
static var version: String {
return Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String
}
}
iTunesIDの調べ方
<YOUR_ITUNES_ID>
の部分は、バージョン情報を取得したいアプリのiTunesIDに置き換えてください。iTunesIDはブラウザでApp Store上のアプリのページを開いたときのURLに表示されています。同様にstoreURLString
もApp StoreのURLに置き換えてください。
アプリバージョンの比較
上記の実装では、1日1回バージョンの更新チェックをするようになっています。アプリバージョンの比較は、major.minor.patch形式のバージョン文字列をドットで分割しInt型に変換(versionToInt(_:)
メソッドを見てください)したもので比較しています。上記のやり方ではminorとpatchは0~999の1000ステップしか値を取れませんが十分でしょう。
storeVersion > currentVersion
となるとき、App Storeに最新版がリリースされていることが分かります。
AppStoreクラスを使う
AppStoreクラスを使うときはViewControllerで以下のようなメソッドを作っておき、viewDidAppear
やUIApplication.willEnterForegroundNotification
をオブザーブしたメソッドなどで呼び出します。
private extension ViewController {
func checkVersion() {
AppStore.checkVersion { (isOlder: Bool) in
guard isOlder else { return }
let alertController = UIAlertController(title: "新しいバージョンがあります!", message: "アップデートしてください。", preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "アップデート", style: .default) { action in
AppStore.open()
})
alertController.addAction(UIAlertAction(title: "キャンセル", style: .cancel))
self.present(alertController, animated: true)
}
}
}
以上の実装で、新しいバージョンがリリースされたことをユーザに伝えることができるようになりました。
Frameworkを作りました
半強制アップデートの仕組みを提供するFrameworkを作りました。以下の続編をご一読ください。