個人開発でアプリを出していると、ダウンロードは伸びているのにレビュー数が二桁から動かない、という時期があります。私も壁紙アプリで長くこれに悩みました。よくある対処は「レビューしてください」の文面を練ることですが、実際に効いたのは文面ではなく、依頼を出す瞬間を変えることでした。
Rork が生成する Expo アプリでも、評価を求める仕組みは数行で入ります。問題はその数行を「いつ呼ぶか」です。iOS のネイティブ評価プロンプトは表示回数に厳しい上限があり、雑に出すと貴重な機会を無駄打ちします。レビュー数を伸ばすためのタイミング設計を、私自身が壁紙アプリで試した結果と ASO への効き方を交えて整理します。
なぜレビュー数が ASO に効くのか
App Store の検索結果やプロダクトページで、ユーザーがインストールを決める材料は限られています。スクリーンショットと、星評価と、件数です。
ここで見落とされがちなのが「件数」の役割です。星4.8でも件数が5件なら、ユーザーは「たまたま身内が付けただけかもしれない」と疑います。同じ4.8でも件数が500件あれば、それは信頼の証拠に変わります。つまりレビューは、星の高さと件数の両輪で初めてコンバージョンに効きます。星を上げる努力と、件数を増やす努力は別物だと分けて考えるのが出発点です。
そして件数は、検索順位そのものにも間接的に影響します。インストール後のコンバージョンと継続率が上がれば、ASO 上のシグナルが改善し、同じキーワードでの露出が伸びる——この循環の入口がレビュー件数です。
やってはいけない3つのタイミング
まず、評価を求めてはいけない瞬間を潰します。
- アプリ起動直後。まだ価値を感じていないユーザーに頼んでも、低評価か無視のどちらかです。
- クラッシュやエラーの直後。最悪のタイミングで、星1を量産します。
- 課金を断られた直後や、広告を見せた直後。気分が下がっている瞬間に頼むと逆効果です。
私が最初にやってしまったのは1番でした。起動時にプロンプトを出していた頃は、平均評価がじわじわ下がっていきました。タイミングを変えただけで、同じアプリの平均が回復したのが、この設計を真剣に考えるきっかけです。
2番は特に厄介です。エラーの直後は避けているつもりでも、「保存に失敗した3分後」に嬉しさの条件が別経路で満たされて発火する、という取りこぼしが起きます。エラーを記録して一定時間は依頼を止める、という消極的なガードを1本入れておくと安全です。後述する実装ではこれを noteError として持たせています。
「嬉しさの瞬間」を定義する
評価を求めるべきは、ユーザーがそのアプリで何か良い体験をした直後です。壁紙アプリなら「気に入った壁紙を保存した」「お気に入りに3枚追加した」瞬間。タスク系なら「タスクを完了した」瞬間です。
ポイントは、その瞬間が「ユーザー自身の達成」であることです。アプリが何かを押し付けた直後ではなく、ユーザーが満足を感じた直後を狙います。さらに、初回利用ではなく数回使ってアプリを気に入った頃合いを重ねると、評価の質が上がります。私の場合は「3セッション以上、かつお気に入り保存を2回以上した直後」を条件にしています。
自分のアプリでこの瞬間が思いつかないときは、下の対応表を出発点にすると考えやすくなります。共通しているのは、画面遷移ではなく「ユーザーが手を動かして何かを成し遂げた直後」を選んでいる点です。
| アプリの種類 | 嬉しさの瞬間 | 条件式の例 |
| 壁紙・画像系 | 気に入った画像を保存し終えた直後 | 保存回数 ≧ 2 かつ セッション ≧ 3 |
| タスク・習慣 | 連続達成が途切れずに一定日数を超えた直後 | 連続日数 ≧ 5 かつ 完了総数 ≧ 10 |
| ツール・変換系 | 処理が成功して結果を書き出した直後 | 成功回数 ≧ 3 かつ 直近に失敗なし |
| ゲーム | ステージ突破やベスト更新の直後 | クリア数 ≧ 5 かつ 直前がリトライ連続でない |
| SNS・共有系 | 自分の投稿に初めて反応が付いたとき | 受信リアクション ≧ 1 かつ セッション ≧ 4 |
もうひとつ、表示位置の話をしておきます。ネイティブプロンプトは モーダルの上には出せません。保存完了のダイアログを閉じきる前に呼ぶと、システム側が黙って無視します。私は「完了トーストが消えた後」に一拍置いて呼ぶ形にしています。
expo-store-review の実装と頻度制限
実装は expo-store-review を使います。ネイティブのプロンプトは iOS の制約で365日あたり3回までしか表示されないため、isAvailableAsync と独自の条件判定を必ず噛ませます。
条件判定を別ファイルに切り出しておくと、あとで数値を調整するときに画面側を触らずに済みます。以下は実際に使っている形をそのまま整理したものです。
// lib/review.ts
import AsyncStorage from "@react-native-async-storage/async-storage";
import * as StoreReview from "expo-store-review";
import * as Application from "expo-application";
const K_SESSIONS = "review.sessions";
const K_DELIGHT = "review.delight";
const K_ASKED_VERSION = "review.askedVersion";
const K_ASKED_AT = "review.askedAt";
const K_ERROR_AT = "review.errorAt";
const MIN_SESSIONS = 3; // 何回起動したユーザーに絞るか
const MIN_DELIGHT = 2; // 嬉しさの瞬間を何回踏んだか
const COOLDOWN_DAYS = 120; // 前回依頼からの最低間隔
const ERROR_QUIET_HOURS = 24; // 直近エラーからの沈黙時間
const num = async (k: string) => Number((await AsyncStorage.getItem(k)) ?? 0);
const bump = async (k: string) =>
AsyncStorage.setItem(k, String((await num(k)) + 1));
/** アプリがフォアグラウンドに戻ったときに1回呼ぶ */
export const noteSession = () => bump(K_SESSIONS);
/** 保存完了・タスク完了など、達成の直後に呼ぶ */
export const noteDelight = () => bump(K_DELIGHT);
/** エラーバウンダリや API 失敗ハンドラから呼ぶ */
export const noteError = () =>
AsyncStorage.setItem(K_ERROR_AT, String(Date.now()));
export async function maybeAskForReview(): Promise<"asked" | "skipped"> {
const version = Application.nativeApplicationVersion ?? "0";
const [sessions, delight, askedVersion, askedAt, errorAt] = await Promise.all([
num(K_SESSIONS),
num(K_DELIGHT),
AsyncStorage.getItem(K_ASKED_VERSION),
num(K_ASKED_AT),
num(K_ERROR_AT),
]);
const now = Date.now();
const day = 24 * 60 * 60 * 1000;
if (sessions < MIN_SESSIONS) return "skipped";
if (delight < MIN_DELIGHT) return "skipped";
if (askedVersion === version) return "skipped";
if (askedAt && now - askedAt < COOLDOWN_DAYS * day) return "skipped";
if (errorAt && now - errorAt < ERROR_QUIET_HOURS * 60 * 60 * 1000) return "skipped";
if (!(await StoreReview.isAvailableAsync())) return "skipped";
if (!(await StoreReview.hasAction())) return "skipped";
await StoreReview.requestReview();
await AsyncStorage.multiSet([
[K_ASKED_VERSION, version],
[K_ASKED_AT, String(now)],
]);
return "asked";
}
呼び出し側は、達成の直後に noteDelight() を打ってから、UI が落ち着いたところで maybeAskForReview() を呼ぶだけです。
async function onWallpaperSaved() {
await noteDelight();
showToast("保存しました");
setTimeout(() => {
void maybeAskForReview();
}, 1200); // トーストが消えてから呼ぶ
}
なぜこの形なのか、条件ごとに理由があります。askedVersion はバージョンごとに一度きりにするためのもので、頻繁にリリースする個人開発では、これだけだと実質毎リリース依頼することになります。そこで COOLDOWN_DAYS を重ねて、時間軸でも縛っています。ERROR_QUIET_HOURS は前述の取りこぼし対策です。
戻り値を "asked" | "skipped" にしているのは、後述する計測のためです。ここで返る "asked" は「依頼を試みた」であって「プロンプトが表示された」ではありません。この区別を曖昧にすると、あとで数字を読み違えます。
iOS の年3回制限は開発者からは制御できないため、システムがプロンプトを実際に出すかは分かりません。だからこそ「出してもいい条件を満たすユーザー」を絞り込み、限られた表示枠を最も評価してくれそうな層に当てるのが重要です。雑に毎回呼ぶと、表示されないまま年間の枠を消費します。
さらに、ユーザーが iOS の設定でアプリ内の評価依頼そのものを切っている場合があります。この設定が入っていると、条件をどれだけ丁寧に組んでも、その端末では一度も表示されません。実装の良し悪しとは無関係に一定割合が抜け落ちる前提で数字を見てください。
iOS と Google Play では挙動が違う
Rork が生成するのはクロスプラットフォームの Expo アプリなので、Android 側にも同じコードが載ります。ところが expo-store-review の内部で呼ばれる仕組みは iOS と Android で別物で、挙動もそろっていません。ここを iOS の感覚のまま扱うと、Android の数字が読めなくなります。
| 項目 | iOS | Android(Google Play) |
| 内部で呼ばれる仕組み | StoreKit のレビュー要求 | Play In-App Review API |
| 表示回数の上限 | 365日あたり3回と明示されている | クォータはあるが具体的な数値は非公開 |
| 上限を超えたとき | 呼んでも表示されない | 何も表示されずにフローが完了する |
| 表示されたかの判定 | できない | できない |
| ユーザー側の無効化 | 設定からアプリ内の評価依頼を切れる | Play ストア側のアカウント状態に依存する |
| 開発中の確認 | Xcode から実行したビルドでは毎回表示される。TestFlight 配布では表示されない | 内部アプリ共有か内部テストトラックで確認する |
実務上いちばん効くのは、最後の行です。TestFlight のビルドで評価プロンプトが出ないのは仕様であって、実装の不具合ではありません。私はここで一度、条件式を疑って半日溶かしました。動作確認は Xcode から直接実行したビルドで行い、条件分岐そのものはログで確認する、と割り切るのが早道です。
Android 側は、Play ストア経由でインストールされたビルドでないと動きません。開発機に直接入れた APK では沈黙します。内部アプリ共有を使うとストア経由の状態で確認できるので、Android の確認はこちらに寄せています。
そして両プラットフォームに共通するのは、「表示されたか」を返してくれない点です。成否で分岐する実装を書きたくなりますが、書けません。返ってくるのは「呼び出しが完了した」という事実だけです。
満足度チェックを挟む設計と Apple の線引き
「アプリは気に入っていますか?」と先に尋ね、はいの人だけネイティブプロンプトに進め、いいえの人はフィードバックフォームへ誘導する——この事前チェックは広く使われています。
ただし線引きが重要です。Apple が禁じているのは、星評価を入力させる自作のダイアログや、ネイティブプロンプトの代替となるカスタム UI です。一方、フィードバックの入口として満足度を尋ねること自体は許容範囲です。私は「満足度を尋ねる→満足な人だけ requestReview を呼ぶ→不満な人は問い合わせへ」という形にしています。これにより低評価が公開レビューに流れる前に、改善要望として受け取れます。
ここで絶対にやってはいけないのは、不満な人を評価から完全に締め出す露骨な設計です。誘導が露骨だと審査で指摘されますし、何より誠実ではありません。満足度チェックはあくまで「適切な出口へ案内する」ためのものだと考えています。
判断に迷ったときは、自作 UI に星やスライダーなど評価そのものを入力させる要素があるかどうかを基準にしています。ある場合は作り直します。「はい/いいえ」の二択で出口を分けるだけなら、評価の入力ではなく導線の分岐です。この線を自分の中で言語化しておくと、審査を待つ間の落ち着きが違います。
条件を厳しくしすぎないための分母の試算
タイミング設計を突き詰めると、条件をどんどん厳しくしたくなります。ところが条件は、そのまま依頼できる人数を削ります。締める前に、手元の数字で分母を一度試算しておくと安心です。
たとえば月間アクティブが3,000人のアプリで、3セッション以上に到達するのが25%、そのうち嬉しさの条件を満たすのが40%だとします。3,000 × 0.25 × 0.40 = 300人。ここから iOS の年3回上限とユーザー側の無効化で目減りし、さらにプロンプトが出た人のうち実際に星を付けるのは一部です。仮に依頼できた人の数%が評価に至るとすると、月あたりの新規レビューは一桁台に着地します。
この試算は、あくまで自分のアプリの数字を入れるための枠組みです。大事なのは絶対値ではなく、条件をひとつ足すたびに分母が掛け算で減るという構造のほうです。「3セッション以上」を「5セッション以上」に変えると、評価の質は上がるかもしれませんが、件数は目に見えて落ちます。
私はまず緩めの条件で分母を確保し、平均評価が下がる兆しが見えたら締める、という順序で調整しています。逆順、つまり最初から厳しく組むと、件数が動かない理由が「条件が厳しすぎるから」なのか「そもそも嬉しさの瞬間の定義がずれているから」なのか、切り分けられなくなります。
効果の測り方
前述のとおり、プロンプトが表示されたかどうかは取得できません。そこで代わりに依頼を試みた回数を記録します。maybeAskForReview の戻り値をイベントとして送り、"skipped" のときはどの条件で落ちたかも一緒に残しておくと、条件調整の材料になります。
そのうえで、App Store Connect の「評価とレビュー」で日次の件数を見て、依頼試行数の推移と並べます。比較は週単位で、同じ曜日構成にそろえるのが読みやすいです。日単位だと曜日の影響が大きく、変化が埋もれます。
もうひとつの注意点として、条件変更とバージョン公開日は分けてください。同じ日に重ねると、数字が動いた理由が新機能なのかタイミング設計なのか判別できなくなります。私はタイミングだけを変えたリリースを一本挟むようにしています。
平均評価は既存の総件数に引きずられるため、短期では動きにくい指標です。件数が数百ある状態で新規が数十件増えても、平均はほとんど動きません。まず件数の増加ペースを見て、平均は数か月単位で追う。この順番にすると、施策の判断を焦らずに済みます。
なお App Store Connect ではバージョン公開時に評価をリセットできますが、件数がゼロに戻ります。件数そのものが信頼の材料になっている段階では、慎重に判断してください。
私の壁紙アプリでは、起動時プロンプトから「嬉しさの瞬間」へ移しただけで、平均評価が0.3ほど回復し、月あたりの新規レビュー件数も明確に増えました。文面を何度書き直しても動かなかった数字が、タイミング設計だけで動いたのは、いま振り返っても象徴的でした。
レビューは買えませんが、出す瞬間は設計できます。まずは自分のアプリで「ユーザーが何かを成し遂げた瞬間」をひとつ書き出し、そこに noteDelight() を1行置くところから始めてみてください。同じようにレビュー数で伸び悩んでいる方の参考になれば幸いです。お読みいただきありがとうございました。