【iOS】続・半強制アップデートの仕組みをカジュアルに実装する

# はじめに

ユーザの端末にインストールされているアプリのバージョンが古いままだと、最新機能やバグ修正が提供できないだけでなく、メンテナンスが難しくなっていきます。ユーザには常に最新バージョンを使ってもらうのが望ましく、アップデートを促す仕組みは積極的に取り入れた方が良いと考えています。

アプリを起動したとき、最新バージョンがApp Storeにリリースされていることをユーザに伝え、アップデートを促す仕組みをこの記事では**半強制アップデート**と呼びます。

以前、「[【iOS】半強制アップデートの仕組みをカジュアルに実装する](https://qiita.com/hituziando/items/fc2ad0642ccdb1258840)」という記事でiTunes Search APIを利用した半強制アップデートの実装を紹介しました。このソースコードは僕が開発しているいくつものアプリに導入されており、これだけ流用するのならばフレームワーク化して一元管理した方が楽になってきます。
ということで、**アプリの最新バージョンがApp Storeにリリースされたらアップデートを促す機能を提供するフレームワーク: SwiftyUpdateKit**を作りました。なるべく少ないコード量で機能を実装できるように意識して作っています。

ソースコードはGitHubに公開しています。

https://github.com/HituziANDO/SwiftyUpdateKit

# SwiftyUpdateKitの使い方

インストール方法はSwiftPM、Carthage、CocoaPodsに対応しています。ビルド済のxcframeworkもReleasesにアップしてあります。詳しくはGitHubのREADMEを見てください。

## 初期化

AppDelegateクラスの`application(_:,didFinishLaunchingWithOptions:)`メソッド内でコンフィグを設定します。以下のコードのコメントを参考にしてください。

```swift:AppDelegate.swift
import SwiftyUpdateKit

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        let config = SwiftyUpdateKitConfig(
            // 現在のアプリバージョン
            // 普通は以下の通りInfo.plistのバージョンを指定すれば良いはずです
            version: Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String,
            // iTunes ID
            // iTunes IDはブラウザでApp Storeのアプリページを開いたときのURLから分かります
            // e.g.) App Store URL: "https://apps.apple.com/app/sampleapp/id1234567890" -> iTunesID is 1234567890
            iTunesID: "1491913803",
            // App StoreのアプリページのURL
            storeURL: "https://apps.apple.com/app/blue-sketch/id1491913803",
            // iTunes Search APIで使う国コード。省略したときはUSの情報を取得します
            // 国コードは↓で調べられます
            // http://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
            // 多言語対応するときはこの国コードを切り替えてください
            country: "jp",
            // アプリバージョンの比較方法
            // 省略したときはX.Y.Z形式のバージョンをstoreVersion > currentVersionかで比較します
            versionCompare: VersionCompare(),
            // アップデートアラートのタイトル
            updateAlertTitle: "新しいバージョンがあります!",
            // アップデートアラートのメッセージ
            updateAlertMessage: "アプリをアップデートしてください。アップデート内容の詳細はApp Storeを参照してください。",
            // アップデートアラートの更新ボタン
            updateButtonTitle: "アップデート",
            // アップデートアラートのキャンセルボタン
            // nilを指定したときは非表示 -> キャンセル不可のためアップデートを強制します
            remindMeLaterButtonTitle: "また後で"
        )

        // コンフィグをセットし初期化
        // 第2引数のクロージャをセットしたときはフレームワークの内部ログを出力します
        SUK.initialize(withConfig: config) { print($0) }

        return true
    }
}
```

iTunes IDの調べ方は[こちら](https://qiita.com/hituziando/items/fc2ad0642ccdb1258840#itunesid%E3%81%AE%E8%AA%BF%E3%81%B9%E6%96%B9)を参考にしてください。

## バージョンチェックする

現在のアプリバージョンとApp Storeにリリースされているバージョンを比較するためには`checkVersion`メソッドを`viewDidAppear`メソッド内で実行します。

```swift:ViewController.swift
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)

    SUK.checkVersion(VersionCheckConditionAlways())
}
```

この一行でバージョン比較を行ない、最新バージョンがApp Storeにリリースされていた場合は以下のようなアップデートアラートを表示します。アラートのテキストはコンフィグで設定したものになります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73490/1010b6f5-9ded-e55c-ef63-94aef689eed9.png" width=240 />

`checkVersion`メソッドの引数の`VersionCheckCondition`には以下の種類があります。

```swift
// 常にバージョンチェックを行ないます
VersionCheckConditionAlways()

// 一日一回バージョンチェックを行ないます
// 1回チェックした後は日付が変わるまでチェックを行ないません
VersionCheckConditionDaily()

// 常にバージョンチェックを行ないません
VersionCheckConditionDisable()
```

この他に独自の条件を指定したい場合は`VersionCheckCondition`プロトコルを実装し、そのオブジェクトを引数に指定してください。

```swift
public protocol VersionCheckCondition: AnyObject {
    /// If returns true, checks the app version.
    func shouldCheckVersion() -> Bool
}
```

[以前の記事](https://qiita.com/hituziando/items/fc2ad0642ccdb1258840)では半強制アップデートの仕組みまでを紹介していました。SwiftyUpdateKitには更に追加の機能を用意しています。

## アップデート後にリリースノートを表示する

SwiftyUpdateKitを使えば、**アプリアップデート後の初回起動時に変更内容を伝えるリリースノートをユーザに表示できます**。以下のコードを御覧ください。先程のコードに`newRelease`以降が足されています。

```swift:ViewController.swift
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)

    SUK.checkVersion(VersionCheckConditionAlways(), newRelease: { [weak self] newVersion, releaseNotes, firstUpdated in
        guard let self = self else { return }
        SUK.showReleaseNotes(from: self, text: releaseNotes, version: newVersion)
    })
}
```

このコードはアプリのバージョンチェックを行ない最新Ver.であったとき、`newRelease`クロージャが呼ばれます。その中で`showReleaseNotes`メソッドを実行すると以下のようなViewControllerが表示されます。`text`に渡している`releaseNotes`は、App StoreのリリースノートをiTunes Search APIで引っ張ってきたものになります。`firstUpdated`フラグは、SwiftyUpdateKitを導入して初めてアップデートしたとき（初回インストール含む）のみ`true`になります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73490/cdeab56c-c0f7-f1c8-02d8-99abf79a33af.png" width=240 >

`showReleaseNotes`メソッドの代わりに任意のViewControllerやViewを作って表示しても構いません。（デフォルトのViewControllerはかなりシンプルなので）

## アプリレビューを要求する

もう一つの追加機能は以下のようなアプリレビューを要求できます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73490/7b519395-1093-f549-e97e-3913fb6fb359.png" width=240 >

以下のコードは、先程のコードに`requestReview`メソッドが足されています。`requestReview`メソッドの引数の`RequestReviewCondition`は、`VersionCheckCondition`プロトコルと同様です。ユーザにレビューをお願いする条件をカスタマイズできます。

```swift:ViewController.swift
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)

    SUK.checkVersion(VersionCheckConditionAlways(), newRelease: { [weak self] newVersion, releaseNotes, firstUpdated in
        guard let self = self else { return }
        SUK.showReleaseNotes(from: self, text: releaseNotes, version: newVersion)
    }) {
        SUK.requestReview(RequestReviewConditionAlways())
    }
}
```

このコードは、

1. 現在のアプリバージョンが最新でないならば、アップデートアラートを表示
2. 最新Ver.にアプリがアップデートされた後、初めて起動された時にリリースノートを表示
3. 最新Ver.のアプリであり、リリースノートも表示済みならば、レビューを要求

という挙動になります。

# まとめ

SwiftyUpdateKitを使えば、簡単に以下の機能をアプリに組み込むことができます。

- アプリの最新バージョンがApp Storeにリリースされたらアップデートを促す
- アプリアップデート後の初回起動時に変更内容を伝えるリリースノートをユーザに表示
- 任意の条件でユーザにレビューを要求

https://github.com/HituziANDO/SwiftyUpdateKit