ANTIGRAVITY LABEN
記事一覧/アプリ開発
アプリ開発/2026-05-26中級

アプリ内レビュー要求の出し分け条件を Antigravity Editor で 5 アプリ統一した数日の記録

個人開発で運用してきた iOS 壁紙アプリ 5 本の SKStoreReviewController 呼び出し条件を、Antigravity Editor のマルチファイル編集機能でひとつの基準にまとめ直したときの数日間の所感を、コード例とあわせて静かに記録しました。

SKStoreReviewControlleriOS27Antigravity338アプリ内レビュー個人開発90app-dev47

きっかけは、深夜に App Store Connect のレビュー画面を眺めていたときの小さな違和感でした。同じ系統の壁紙アプリを 5 本運営しているのに、レビュー件数の集まり方も、平均評価も、傾向がそれぞれ違いすぎていたのです。アプリ自体の質がそこまで変わるはずもなく、出し分けロジックの方に原因があるはずだ、と当たりをつけて Antigravity Editor を立ち上げました。

2014年から個人開発でアプリを公開し続け、累計5,000万ダウンロードを超えてきました。長く運用していると、こうした「気づかないうちに各アプリの細かい挙動がずれていく」現象が、地味ながら一番こたえます。今回はその修正作業を、Antigravity Editor のマルチファイル編集と並走しながら数日かけて整えたときの記録です。

ばらつきの正体は、運用初期にコピペで写した時期差

最初に Antigravity Editor で 5 つのリポジトリを並列に開き、SKStoreReviewController.requestReview を呼んでいる箇所を一気に列挙させました。出てきた条件は、おおよそ次のようなものです。

  • アプリ A: 起動回数 5 回以上、最終ダイアログから 90 日以上
  • アプリ B: 起動回数 3 回、間隔 60 日
  • アプリ C: 「お気に入り」を 1 回以上タップした後の初回起動時
  • アプリ D: 起動回数 10 回、間隔指定なし
  • アプリ E: 起動 4 回、最終ダイアログ判定はあるが UserDefaults のキー名がタイポでずっと書き込まれていない

アプリ E が一番きつい発見でした。レビュー要求は出ているのに「前回いつ出したか」が記録されていない、つまりユーザーが拒否しても近いうちにまた出てしまう状態が、おそらく長い期間続いていたのです。低評価が他より多めなのもこれで腑に落ちました。

宮大工だった両祖父の影響なのか、こうした細かい不整合を見ると胸の奥がざわつきます。小さな仕事ほど丁寧に、というのは祖父たちから受け取った数少ない明文化されていない教えで、UX 上の小さなテキストやダイアログも同じだと考えています。

共通条件をひとつのコーディネーター型に切り出す

まず方針として、各アプリに散らばっていた判定ロジックを ReviewPromptCoordinator.swift に集約し、アプリ固有の条件はクロージャで差し込む形にしました。Antigravity Editor に「アプリ A〜E のリポジトリそれぞれに同じファイルを作成して、import パスは各アプリ向けに整える」とだけ伝えると、ファイル差分のプレビューが 5 タブに分かれて表示され、目視で確認しながら適用できました。

import StoreKit
import UIKit
 
public final class ReviewPromptCoordinator {
    public struct Config {
        let minLaunchCount: Int
        let minDaysSinceLastPrompt: Int
        let additionalCondition: () -> Bool
    }
 
    private let config: Config
    private let defaults: UserDefaults
    private static let lastPromptKey = "review.prompt.lastShownAt.v2"
    private static let launchCountKey = "review.prompt.launchCount.v2"
 
    public init(config: Config, defaults: UserDefaults = .standard) {
        self.config = config
        self.defaults = defaults
    }
 
    public func registerLaunch() {
        let current = defaults.integer(forKey: Self.launchCountKey)
        defaults.set(current + 1, forKey: Self.launchCountKey)
    }
 
    public func requestIfEligible(in scene: UIWindowScene) {
        guard isEligible() else { return }
        SKStoreReviewController.requestReview(in: scene)
        defaults.set(Date(), forKey: Self.lastPromptKey)
    }
 
    private func isEligible() -> Bool {
        let count = defaults.integer(forKey: Self.launchCountKey)
        guard count >= config.minLaunchCount else { return false }
        if let last = defaults.object(forKey: Self.lastPromptKey) as? Date {
            let days = Calendar.current.dateComponents([.day], from: last, to: Date()).day ?? 0
            guard days >= config.minDaysSinceLastPrompt else { return false }
        }
        return config.additionalCondition()
    }
}

UserDefaults のキー名にあえて .v2 という接尾辞を付けたのは、過去にタイポで書き込めていなかったアプリ E の互換を切るためです。ここは旧キーから移行するのではなく、リセットして「いま壊れている状態をきれいに置き換える」決断をしました。古い書き込みに引きずられるくらいなら、一度だけ多めにレビュー要求が出る方が運用上は健全だと判断しています。

アプリ固有の条件はクロージャで素朴に書く

共通基盤ができたので、アプリごとの「気持ちよく要求できる瞬間」をクロージャに閉じ込めます。たとえば、お気に入り壁紙を 3 枚以上保存しているユーザーにだけ表示する、というように。

let coordinator = ReviewPromptCoordinator(config: .init(
    minLaunchCount: 5,
    minDaysSinceLastPrompt: 120,
    additionalCondition: {
        FavoriteStore.shared.count >= 3
    }
))

5 アプリそれぞれの AppDelegateApp 構造体に近い位置で初期化するだけで、判定の流れが一気に読みやすくなりました。Antigravity Editor の差分ビューを横並びにしたまま、5 ファイルを順番に編集し、適用後に自分で読み直して納得できる粒度に揃えています。AI に任せきりにしないで、最後の差し込みは自分の指でやる、というのが個人的な落としどころです。

並列レビューの中でエージェントが拾ってくれた抜け

ありがたかったのは、ファイル差分のプレビュー中にエージェントが残してくれたコメントです。アプリ B の旧コードでは、起動回数のカウントが「設定画面を開いた回数」と二重に増える実装になっていました。私は気づかずに統一作業を進めていて、新しいコーディネーターに渡しているのは正しい launch count なのですが、旧コードを呼んでいる別の場所が残ったままだったのです。

エージェントが「この呼び出しは新しい Coordinator に移行した方が整合します」と差分の下に淡々と書いてくれていたので、もう一度該当ファイルを開いて、旧 API を削除しました。AI が最強だとか神だとか、そういう持ち上げ方はしっくりこないのですが、こういう「人間が手を抜きそうな端」を静かに見ていてくれる感覚はかなり助かります。

アプリ E の不具合をどう伝えるか

5 アプリのうちアプリ E は、レビュー要求の「前回出した日時」が記録されていなかった期間があるため、過去に拒否したユーザーが何度も同じダイアログを見ていた可能性があります。これはユーザーへの誠実さの問題でもあると考え、次のバージョンのリリースノートに、控えめながら不具合と修正の事実を書きました。

「内部処理の不具合により、レビュー要求の表示間隔が想定より短くなることがありました。本バージョンで修正しています」という一文だけです。扇情的に書く必要はないけれど、無かったことにもしたくない、という温度感で文面を整えました。離れて暮らす子どもたちに対しても、私はこういう仕事の姿勢でいたいと思っています。

レビュー要求のタイミングを慎重に見ていく

統一作業の後で、念のために minLaunchCount と「お気に入り枚数しきい値」だけを 2 群に分けて、AdMob のリワード視聴回数やリテンションへの間接影響を Firebase で軽く眺める計画も立てました。これは別の記事に分けたい話なので、ここでは深掘りしませんが、レビュー要求のタイミングは、評価平均だけでなく次回起動率にも効いてきます。

数日経った所感

統一前は、レビューが集まりやすいアプリと、ほぼ集まらないアプリで件数に数倍の差がついていました。揃えてからまだ日が浅いので断定はできませんが、低評価の流入は落ち着いてきた印象です。ここで「劇的に改善した」と書きたくなる場面なのかもしれませんが、たかだか数日のデータでそう言うのは誠実ではありません。実装は静かに終え、データはこれから数か月、淡々と見ていきます。

次に取り組むこと

同じ要領で「アプリ内通知の文言」と「Empty State の案内文」も、5 アプリで統一していく予定です。1997年、16歳のときに独学でプログラミングを始めて以来、長く同じ場所でコードを書き続ける気持ちよさをずっと探してきました。こうした地味な統一作業は、その気持ちよさにいちばん近いところにあると感じています。

お読みいただきありがとうございました。同じように複数アプリを運用している個人開発の方の参考になれば幸いです。

シェア

お読みいただきありがとうございます

Antigravity Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

アプリ開発2026-07-04
課金済みなのに広告が消えない — StoreKit 2 の Transaction.updates を起動直後に取りこぼす競合を Antigravity と塞いだ記録
起動直後に届く StoreKit 2 のトランザクションを取りこぼす競合を、View 寿命ではなくアプリ寿命にリスナーを固定して塞いだ実装記録。currentEntitlements での照合、finish() の徹底、StoreKitTest での回帰テスト化まで。
アプリ開発2026-06-23
StoreKit 2 で「課金したのに使えない」と言われたとき — サブスク権利状態のズレを直す運用メモ
StoreKit 2 のサブスクリプションは実装より運用でつまずきます。currentEntitlements と subscription.status のズレ、Transaction.updates の取りこぼし、未 finish トランザクション、復元と返金の扱いを、本番で効いた整流ロジックとともに記録します。
アプリ開発2026-05-16
Antigravity で新 iPhone の解像度対応を乗り越えた話 — iPhone Air / 17 Pro 対応で29箇所ハマった実例
iPhone Air(420×912)・17 Pro(402×874)など新モデルへの解像度対応でハマった実例を紹介します。Beautiful HD Wallpapers の更新時に DefineManager.h を29箇所修正した経験と、Antigravity でどこまで自動化できたかを正直に書きます。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →