2026 年の 5 月上旬、私が運営している壁紙アプリ 4 タイトルの iOS 側を一度に動かす作業に入りました。きっかけは Apple からの StoreKit 1 廃止アナウンスが現実味を帯びてきたことと、AdMob のメディエーション設定を 3 タイトル横断で揃え直す必要が出てきたことです。Mac mini M5 1 台しか手元にないので、Xcode を 4 つ並べて手作業で進めるとどう考えても潰れます。そこで Claude Code に多くの工程を渡してみた所感を残しておきます。
廣川政樹と申します。アーティスト・個人開発者として 2014 年から壁紙アプリを運営しており、4 つの主要タイトル(綺麗な壁紙・Dolice 壁紙・浮世絵壁紙・イラスト壁紙)の累計ダウンロードは 5,000 万を超えました。今回の作業は、その 4 タイトルを横断で動かす久々の大きな更新で、個人開発の現場でしか出てこない判断がいくつも積み重なりました。
並行作業の前提と、最初に決めた「触らない範囲」
最初の半日は実装ではなく「触らない範囲」を決めることに使いました。4 タイトル同時に変える項目を増やすと、リジェクトが 1 本でも起きたときに全タイトルが止まります。私は次のような切り分けにしました。
共通で動かす : StoreKit 2 への移行、AdMob SDK のバージョン、ATT ダイアログの文言、プライバシーマニフェスト
タイトルごとに分離する : 価格構成、新規 IAP の追加、UI の改修
今回は触らない : バックエンド api.dolice.asia の応答スキーマ、画像ストレージのリダイレクト、レビュー導線
この境界線を Claude Code に渡す CLAUDE.md の冒頭に書いておくと、提案が必要以上に広がらなくなります。実際、Sonnet 4.6 を Claude Code から呼んだとき、UI 改修を勝手に提案してくる場面が初日に何度かありましたが、CLAUDE.md に「UI は今回触らない」と書いてからは、提案がアーキテクチャ寄りに収束しました。
Claude Code を 4 リポで動かす Xcode ワークスペースの並べ方
Mac mini M5 1 台での運用なので、Xcode のメモリ使用量が一番のボトルネックです。私はターミナル側で Claude Code を 4 リポ並行で動かし、Xcode は 1 タイトルだけ立ち上げる構成に落ち着きました。
# 4 タイトル並行で Claude Code を立ち上げる ghostty + tmux 構成
tmux new-session -d -s bw 'cd ~/repos/beautiful_wallpapers && claude'
tmux new-session -d -s dw 'cd ~/repos/dolice_wallpapers && claude'
tmux new-session -d -s uk 'cd ~/repos/ukiyo-e && claude'
tmux new-session -d -s il 'cd ~/repos/illustration_wallpapers && claude'
# Xcode は 1 つだけ。残り 3 つは Claude Code に Swift Build CLI を叩かせる
tmux attach -t bw
実測でメモリは 28 GB のうち 18〜22 GB で安定し、Xcode を 2 つ以上立ち上げたときに観測したスワップ発生(スワップ 8 GB 超)はなくなりました。Claude Code 単独のメモリ消費は 1 リポあたり 600〜900 MB 程度で、4 並列でも 4 GB に収まります。
StoreKit 2 移行で個人開発者がつまずく 5 つの分岐点
ここからが本題です。Apple の StoreKit から StoreKit 2 への移行は、ドキュメントを読めば手順自体は素直なのですが、個人開発で複数タイトルを扱うと別の難しさが出てきます。私が今回出会った分岐点を 5 つに整理します。
1. レシート検証を「サーバー or クライアント」のどちらに置くか
新 API の Transaction.currentEntitlements を使えばクライアントだけで完結します。ただし、サブスクの不正利用対策を真面目にやるならサーバー側で検証したいところです。私は 4 タイトルとも App Store Server API を api.dolice.asia 側に新エンドポイントを切って受け取る方式に統一しました。クライアントは fallback として Transaction.currentEntitlements も読み、両方が一致しない場合だけサーバー判定を優先するようにしています。
2. プロモーション IAP の Transaction.updates の購読タイミング
App 起動時の最初の Task で Transaction.updates を購読しないと、ユーザーが App Store からプロモーションオファーを踏んだときに購入完了イベントを取り損ねます。Claude Code に「@main の中で Task.detached で購読を張って」と頼んだのですが、初稿は App.init で同期的に呼ぶ実装で、起動が 200ms ほど遅れました。明示的にバックグラウンドタスクとして切る指示を出すまで気づきませんでした。
3. Product.products(for:) のキャッシュ戦略
StoreKit 2 の Product.products(for:) は内部キャッシュがあるとはいえ、毎回呼ぶとオフライン時にスピナーが回り続けます。私は UserDefaults に最後に取得した Product の displayPrice と id を保存し、ネットワーク不可時はキャッシュを返す実装にしました。Claude Code が提案した最初のコードは Codable で Product 全体を保存する形でしたが、Product は Codable 適合ではないので動きません。型の制約をプロンプトに含めるかどうかで提案精度が変わるところです。
4. テストフライトと本番の価格差
サンドボックスの displayPrice は実機の本番価格と異なることがあります。AdMob のリワード経由でプロ機能を解放するロジックを書いていると、サンドボックスでは ¥120 表記、本番では ¥150 表記、という差が起きます。価格表示部分を「サーバーから取った推奨価格」と「StoreKit が返す displayPrice」の二系統に切ったところ、本番のレビュー対応で 1 ターンも往復しませんでした。
5. 旧 SKPaymentQueue ベースのリスナーを「消し忘れる」
私が一番ハマったのはここです。SKPaymentTransactionObserver を残したまま Transaction.updates を購読すると、IAP の state 通知が二重で飛んできます。Crashlytics で「finishTransaction が 2 回呼ばれた」というクラッシュが本番リリース直前の TestFlight で 4 件出て、最後のステージングで気づきました。Claude Code に「旧 StoreKit のリスナー痕跡を grep して列挙してくれ」と頼むと、SKPaymentQueue.default() を含む箇所を 4 タイトル合計で 17 箇所抽出し、ここから順に潰しています。
AdMob と RevenueCat の判定順序を任せるときの落とし穴
今回の作業では RevenueCat は使わず、Apple 公式の StoreKit 2 + 自前サーバー検証で組みました。ただし AdMob のリワード広告を踏んだユーザーをプロ機能に昇格させる「reward_action_count」という API フィールドが旧来から存在し、これを StoreKit のサブスクと OR で扱う必要があります。
@MainActor
final class Entitlement : ObservableObject {
@Published private ( set ) var isPro: Bool = false
func refresh () async {
let subscribed = await checkSubscription () // StoreKit 2 由来
let rewardUnlocked = await checkRewardUnlock () // AdMob+サーバー由来
let serverGrant = await checkServerEntitlement () // 自前 API
isPro = subscribed || rewardUnlocked || serverGrant
}
private func checkSubscription () async -> Bool {
for await result in Transaction.currentEntitlements {
if case . verified ( let tx) = result, tx.productType == .autoRenewable {
return true
}
}
return false
}
}
Claude Code に「サブスクとリワードを OR で判定するヘルパーを書いて」と頼んだ際、最初は短絡評価でサブスクが true なら他の確認を飛ばす実装になっていました。実運用では、サブスクのキャンセル直後にリワード残期間で延命するパターンがあるので、すべての判定を必ず通したい場面があります。これも結局はプロンプトで「常に 3 経路すべて判定して」と指示し直すまで気づきませんでした。
段階公開のスケジュール設計
4 タイトル同時の段階公開はリスクが大きいので、私は以下の順序で出しました。
Day 1 : 浮世絵壁紙(最も DAU が少ないタイトル)に 1% リリース
Day 3 : 1% → 10% に拡大、Crashlytics と AdMob レポートを 24 時間観察
Day 4 : 10% で問題がなければイラスト壁紙にも同じスキームで 1% 開始
Day 7 : 浮世絵壁紙を 100% に上げると同時に、Dolice 壁紙を 1% 開始
Day 10 : 綺麗な壁紙を最後に 1% 開始(収益寄与が最大なので最も慎重に)
この順序で、Day 3 にイラスト壁紙の Transaction.updates 二重発火を検知し、綺麗な壁紙のリリース前に修正できました。すべてを同時に上げていたらフルロールバックで広告収益が 2 日分飛んでいた計算になります。
Claude Code に渡してよかったタスクと、渡してダメだったタスク
5 月の作業を振り返ると、Claude Code に渡してよかったのは次のような工程でした。
4 リポ横断で同じパターンの diff を当てる作業(SKPaymentQueue 痕跡の列挙と置換)
ATT ダイアログ文言とプライバシーマニフェストの整合性チェック
AdMob と StoreKit の OR 判定ヘルパーの初稿生成
レビュー前のリリースノート 4 言語版の素案作成
逆に渡してダメだったのは次のような場面です。
価格表記の和文校正(敬体・常体のゆらぎが残った)
TestFlight でのみ再現するクラッシュの再現コード生成(再現せず、後で手動再現が必要)
段階公開のパーセンテージ判断(営業日数や DAU 推移を考えずに「すぐ 50%」と提案してくる)
私が次に試したいこと
5 月の作業はひとまず段階公開の 3 タイトル分で大きなクラッシュが出ていない状態に落ち着きました。次は、Claude Code の Hooks を使って git commit 前に「SKPaymentQueue 痕跡が残っていないか」「Transaction.updates 購読が Task.detached になっているか」を自動チェックする仕組みを足そうとしています。
並行作業は人手では限界がありますが、Claude Code に「触らない範囲」と「絶対に守る順序」をしっかり伝えれば、Mac mini 1 台でもここまでは回ります。同じように壁紙やユーティリティアプリを複数本動かしている個人開発者の方は、まず 1 タイトルだけ Claude Code に通して 1 週間運用してみる、というところから入るのが一番事故が少ないように思います。
私自身まだ模索の途中ですが、参考になれば嬉しいです。