きっかけは、深夜に 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 アプリそれぞれの AppDelegate か App 構造体に近い位置で初期化するだけで、判定の流れが一気に読みやすくなりました。Antigravity Editor の差分ビューを横並びにしたまま、5 ファイルを順番に編集し、適用後に自分で読み直して納得できる粒度に揃えています。AI に任せきりにしないで、最後の差し込みは自分の指でやる、というのが個人的な落としどころです。
並列レビューの中でエージェントが拾ってくれた抜け
ありがたかったのは、ファイル差分のプレビュー中にエージェントが残してくれたコメントです。アプリ B の旧コードでは、起動回数のカウントが「設定画面を開いた回数」と二重に増える実装になっていました。私は気づかずに統一作業を進めていて、新しいコーディネーターに渡しているのは正しい launch count なのですが、旧コードを呼んでいる別の場所が残ったままだったのです。
エージェントが「この呼び出しは新しい Coordinator に移行した方が整合します」と差分の下に淡々と書いてくれていたので、もう一度該当ファイルを開いて、旧 API を削除しました。AI が最強だとか神だとか、そういう持ち上げ方はしっくりこないのですが、こういう「人間が手を抜きそうな端」を静かに見ていてくれる感覚はかなり助かります。
アプリ E の不具合をどう伝えるか
5 アプリのうちアプリ E は、レビュー要求の「前回出した日時」が記録されていなかった期間があるため、過去に拒否したユーザーが何度も同じダイアログを見ていた可能性があります。これはユーザーへの誠実さの問題でもあると考え、次のバージョンのリリースノートに、控えめながら不具合と修正の事実を書きました。
「内部処理の不具合により、レビュー要求の表示間隔が想定より短くなることがありました。本バージョンで修正しています」という一文だけです。扇情的に書く必要はないけれど、無かったことにもしたくない、という温度感で文面を整えました。離れて暮らす子どもたちに対しても、私はこういう仕事の姿勢でいたいと思っています。
レビュー要求のタイミングを慎重に見ていく
統一作業の後で、念のために minLaunchCount と「お気に入り枚数しきい値」だけを 2 群に分けて、AdMob のリワード視聴回数やリテンションへの間接影響を Firebase で軽く眺める計画も立てました。これは別の記事に分けたい話なので、ここでは深掘りしませんが、レビュー要求のタイミングは、評価平均だけでなく次回起動率にも効いてきます。
数日経った所感
統一前は、レビューが集まりやすいアプリと、ほぼ集まらないアプリで件数に数倍の差がついていました。揃えてからまだ日が浅いので断定はできませんが、低評価の流入は落ち着いてきた印象です。ここで「劇的に改善した」と書きたくなる場面なのかもしれませんが、たかだか数日のデータでそう言うのは誠実ではありません。実装は静かに終え、データはこれから数か月、淡々と見ていきます。
次に取り組むこと
同じ要領で「アプリ内通知の文言」と「Empty State の案内文」も、5 アプリで統一していく予定です。1997年、16歳のときに独学でプログラミングを始めて以来、長く同じ場所でコードを書き続ける気持ちよさをずっと探してきました。こうした地味な統一作業は、その気持ちよさにいちばん近いところにあると感じています。
お読みいただきありがとうございました。同じように複数アプリを運用している個人開発の方の参考になれば幸いです。