SKStoreReviewControllerのベストプラクティス

SKStoreReviewController利用のベストプラクティスについて、プログラム・デザインなどいくつかの視点から見つつ解説します。

# Overview
[SKStoreReviewController](https://developer.apple.com/documentation/storekit/skstorereviewcontroller)はiOS10.3以降で利用可能になった、ユーザーへシステムプロンプトでレビュー依頼するためのクラスです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114709/50082136-04fa-320a-15a4-898b51277aaf.png" width=300 />
([^1]Human Interface GuidelineのRatings and Reviewsのページで掲載されているシステムプロンプトの例)

[Scene Based Lifecycle](https://developer.apple.com/documentation/uikit/app_and_environment/scenes)以前は[requestReview()](https://developer.apple.com/documentation/storekit/skstorereviewcontroller/2851536-requestreview), 以後は[requestReview(in:)](https://developer.apple.com/documentation/storekit/skstorereviewcontroller/3566727-requestreview)のclass funcを呼び出すと上記画像のようなプロンプトが表示されます。

おそらく、すでに世界中のさまざまなプロダクトで利用されているプロンプトだと思いますが、**実はAppleによってレビュー依頼のベストプラクティスが記載されているページがあります。**この記事では、実装・HIG・ドキュメントなどをいくつか合わせて読み解きながらベストプラクティスについて解説していきます。

# ベストプラクティスを学ぶ
HIGの[Ratings and Reviews](https://developer.apple.com/design/human-interface-guidelines/ios/system-capabilities/ratings-and-reviews/)のページを読むと以下のようなプラクティスが紹介されています。簡単に翻訳すると以下のようになります。

1. **評価を求めるのは、ユーザーがアプリに興味を持ってくれた後にしましょう。**
2. **ユーザーが一刻を争う作業やストレスの多い作業をしているときは、邪魔をしないようにしましょう**
3. **しつこく尋ねないようにしましょう**

([^2] Rating and Reviewsページの心に留めておくべき考慮事項として紹介されているプラクティス)

Human Interface Guidelineはあくまでガイドラインのため、具体的にどうするか？という情報は記載されていませんが、開発者用の[Requesting App Store Reviews](https://developer.apple.com/documentation/storekit/requesting_app_store_reviews)というサンプルコードを読むと

> [^3]Use best practices for prompting users to leave a review for your app in the App Store.

という記述が先頭にあり、レビュー依頼についてベストプラクティスが存在している(少なくともAppleのチームは認識しているであろう)ことが分かります。

このページで紹介されるベストプラクティスをまとめると3点です。

1. **アプリのバージョンをストレージ(サンプルでは[UserDefaults](https://developer.apple.com/documentation/foundation/userdefaults))に保存し、以前レビューを尋ねたのと同じバージョンでは再度レビューを尋ねないようにする。**
2. **ユーザーが重要なタスクを達成した回数をストレージに保存し、それがプロジェクトごとに定義した閾値を超えた場合だけレビューを尋ねるようにする。**
3. **1と2の条件をパスした場合でもユーザーが操作を継続中でないか確認し、少しディレイを掛けた後にユーザー操作の妨害になってしまう場合はリクエストを取りやめ、操作中でない場合のみリクエストする。**

これらを論理コードで表現すると以下のようになります。

```swift
guard 重要なタスクの達成回数 >= 閾値 else {
    return // 早期リターン
}

guard 現在のアプリバージョン != 最後にユーザーにレビューを依頼した時のアプリバージョン else {
    return // 早期リターン
}

DispatchQueue.main.asyncAfter(deadline: .now + 2.0 //任意の秒数) {
    if !ユーザーが操作中 {
        SKStoreReview Controller.requestReview(in: view.window!.windowScene)
    }
}

```

先程のHIGの記述と見比べてもらうと、

- 達成回数と閾値での判定⇄「評価を求めるのは、ユーザーがアプリに興味を持ってくれた後にしましょう。」
- 各バージョンで1度だけ依頼可能な制約⇄「しつこく尋ねないようにしましょう」
- ディレイ後に操作中でないことを確認⇄「ユーザーが一刻を争う作業やストレスの多い作業をしているときは、邪魔をしないようにしましょう」

というように、ガイドラインの内容を綺麗に実現しているのが分かります。

HIGの「しつこく尋ねないようにしましょう」を詳しく見ていくと

> [^4]評価依頼の間隔は少なくとも1～2週間空け、ユーザーがアプリに対してさらにエンゲージメントを示した後にのみ、再度評価を依頼します。

という記述があるため、達成回数判定の閾値は「ユーザーが初回利用から1~2週間ぐらいで達成しそうなぐらい」に調整しておくと良さそうです。

加えて、各バージョンで1度だけレビュー依頼可能な設計にしておくと、2回目以降のレビュー依頼はアプリのリリース頻度に依存するようになります。1週間スプリントだと1週間ですし、1ヶ月スプリントだと1ヶ月になります。

1と2の条件を組み合わせて利用しており、閾値の定義が適切、かつスプリントのサイクルが1週間以上の場合は、「評価依頼の間隔は少なくとも1～2週間空け」を満たし続けることが可能になります。

# ベストプラクティスを実装する

いくつか改善点はありますが、こちらに実現例を実装しました。あくまで参考の1例程度にお考えください。

https://gist.github.com/yosshi4486/2e7486c80281db1e86280e75ceafb5d0

Appleのサンプルでは`kCFBundleVersionKey`を使ってバンドルのバージョンを使っていますが、こちらのサンプルではMarketing Versionを利用してグローバルな計算プロパティからバージョンを取得できるように実装しています。

```swift
var currentAppVersion: String? {

    // Get the current app version.
    let infoDictionaryKey = "CFBundleShortVersionString"
    guard
        let currentVersion = Bundle.main.object(forInfoDictionaryKey: infoDictionaryKey) as? String
    else {
        fatalError("Expected to find a app version in the info dictionary")
    }
    return currentVersion

}
```

処理を意味的にスムーズにするために、`canRequestReview`と`requestReview(in:conditionAfterWait:)`を分けて実現しました。

`canRequestReview`ではベストプラクティスの1と2に相当する判定を行なっています。加えて、重複してリクエストされるのを防ぐために`DispatchWorkItem`を利用して以前のタスクがあればキャンセルを実行してから新規リクエストをスケジュールするようにしています。

```swift
var canRequestReview: Bool {
    print("Process completed \(processCompletedCount) time(s)")
    print("Current app version: \(currentAppVersion ?? "nil")")
    return processCompletedCount >= thretholdCountForReviewRequest && currentAppVersion != lastVersionPromptedForReview
}

// この設計は良くないですが、代案も特に思いついてないのでこのままにしています。何か良い案あればコメントしていただけると助かります。
func requestReview(in windowScene: UIWindowScene, conditionAfterWait condition: @escaping () -> Bool) {
    // Cancel previous request.
    requestReviewWorkItem?.cancel()

    // Make new request work.
    requestReviewWorkItem = DispatchWorkItem(block: { [weak self] in
        if condition() {
            SKStoreReviewController.requestReview(in: windowScene)
            self?.lastVersionPromptedForReview = self?.currentAppVersion
        }
    })

    // Scedule the request.
    DispatchQueue.main.asyncAfter(deadline: .now() + waitTimeForReviewRequest, execute: requestReviewWorkItem!)
}
```

`requestReview(in:conditionAfterWait:)`の`conditionAfterWait`には、ユーザーの行動を見て操作中かどうかの判定値を渡します。

```swift
let requestReviewManager = RequestReviewManager(currentAppVersion: currentAppVersion)

if requestReviewManager.canRequestReview {
    requestReviewManager.requestReview(in: view.window!.windowScene!) {
        /*
        例えば、クロージャで[weak self]をキャプチャして、
        `return self?.navigationController?.topViewController == self`
        と確認する場合は「画面移動をしていないということは、ユーザーが操作をしていない」と判断するということになる。

        UITextViewが表示されている画面など、インタラクティブ性が高い場合はこの条件だけでは不十分であるが
        単純な画面の場合はこれくらいで大丈夫。この判定はプロジェクトの要件によって様々になる。
        */
        return true
    }
}

```

# Wrap up
この記事では、Appleがサンプルコードで記載したレビュー依頼のベストプラクティスについて紹介・解説しました。

サンプルコードでは、HIGのプラクティスと対応して

1. **ユーザーが重要なタスクを達成した回数が閾値を超えているか判定**
2. **各バージョンで1度だけ依頼するように判定**
3. **ディレイを掛け、ユーザーが操作中でないことを確認した後にレビューをリクエスト**

というベストプラクティスが実現されていました。そして、最後に実装の1例を紹介しました。

レビューをちょうど良いタイミングで依頼できればAppStoreで良いレビューが多くなり検索で表示される回数も多くなることが見込め、プロダクトにとっては良いことが多いので、この記事を読んで興味を持っていただけたのならプログラマー・マーケター・デザイナーの方々で一度実現の相談をしてみると良いのではないでしょうか。

[^1]: Apple inc., AppRating_2x.png, System Rating and Review Prompts, Ratings and Reviews, https://developer.apple.com/design/human-interface-guidelines/ios/system-capabilities/ratings-and-reviews/, viewed: 2021/07/28
[^2]: Apple inc., Ratings and Reviews 3~5 Paragraph, Human Interface Guideline, https://developer.apple.com/design/human-interface-guidelines/ios/system-capabilities/ratings-and-reviews/, viewed: 2021/07/28
[^3]: Apple inc., Requesting App Store Reviews, Apple Developer, https://developer.apple.com/documentation/storekit/requesting_app_store_reviews, viewed: 2021/07/28)
[^4]: Apple inc., Don’t be a pest, Ratings and Reviews, https://developer.apple.com/design/human-interface-guidelines/ios/system-capabilities/ratings-and-reviews/, viewed: 2021/07/29