2014年から個人でアプリを作り続けてきて、収益の柱はずっと AdMob の広告でした。壁紙や癒し系のアプリにバナーとインタースティシャルを置き、eCPM の国別・時間帯別の揺れを毎日眺めながら最適化する。その積み重ねで生活が成り立っていた時期が長くありました。
ただ、広告収益には独特の不安定さがあります。年末に単価が跳ね、夏場に沈む。アプリ単体の評価とは別の力学で月の数字が上下する。ある時期から私は「使ってくれている人から、直接いただく形に変えられないか」と考えるようになりました。サブスクリプションは、その問いへの一つの答えです。
この記事は、Rork Max が生成する Swift コードを土台にしながら、StoreKit 2 で月額課金を「事業として」設計するための実務メモです。きれいな成功談ではなく、どこを機械に任せ、どこは自分の判断で詰めるべきか、という線引きを共有できればと思っています。
なぜ、広告の隣にサブスクを置くのか
広告とサブスクは、収益の「形」がまったく違います。
広告は表示回数に比例する変動収益です。100万インプレッションが翌月に半分になれば、収益も半分になります。一方サブスクは、一度有料になった人が継続する限り積み上がるストック収益です。先月の有料会員は、何もしなくても今月の売上の土台になってくれます。
私が広告運用で痛感したのは、「ユーザーが増えても、一人あたりの単価は自分でほとんど動かせない」ことでした。eCPM は広告市場が決めます。サブスクなら、提供する価値と価格を自分で設計できます。ここが、個人開発者にとって決定的な違いだと考えています。
もちろん広告を捨てる必要はありません。無料ユーザーには広告、価値を感じた人にはサブスク、という二層構造が現実的です。本稿はその「サブスク側」をどう組むかに絞ります。
Rork Max に任せる部分、自分で握る部分
Rork Max はネイティブ Swift アプリを生成する製品です。StoreKit 2 を使った購入フローの足場(プロダクト取得・購入ボタン・ペイウォール画面)まではかなりの精度で出してくれます。Xcode も Mac も持たずに App Store 公開まで届くのは、数年前を思えば信じがたい変化です。
ただ、ここで一つ釘を刺しておきます。生成されたコードを「そのまま本番で使える完成品」として扱うのは危険です。サブスクの肝は UI ではなく、権利状態(エンタイトルメント)を取り違えないことにあります。
私が必ず自分の手で握るのは次の三点です。第一に、購入・更新・解約・返金が起きたときに権利状態を一意に決めるロジック。第二に、アプリ再起動やデバイス移行をまたいでも復元できる検証フロー。第三に、サーバー側を持つなら App Store Server Notifications との突き合わせ。生成コードはここを簡略化しがちなので、レビューの目を一番強く向けます。
StoreKit 2 で「権利状態」を取り違えない
StoreKit 2 の設計思想は明快で、Transaction.updates という非同期シーケンスを購読し、検証済みトランザクションから現在の権利を組み立てます。最小構成を示します。
import StoreKit
@MainActor
final class SubscriptionStore : ObservableObject {
@Published private ( set ) var products: [Product] = []
@Published private ( set ) var activeEntitlement: String ? = nil
private let productIDs = [ "pro_monthly" , "pro_yearly" ]
private var updatesTask: Task< Void , Never > ?
init () {
// アプリ起動直後に監視を開始する。ここを忘れると
// バックグラウンド更新や別デバイスでの購入を取りこぼす
updatesTask = listenForTransactions ()
Task { await refresh () }
}
func loadProducts () async {
do { products = try await Product. products ( for : productIDs) }
catch { products = [] } // 取得失敗時は空にして購入導線を出さない
}
func purchase ( _ product: Product) async throws {
let result = try await product. purchase ()
switch result {
case . success ( let verification) :
let transaction = try checkVerified (verification)
await transaction. finish () // finish を忘れると更新が再通知され続ける
await refresh ()
case .userCancelled, .pending :
break
@unknown default:
break
}
}
// 現在有効な権利を「現在の事実」から組み直す
func refresh () async {
var current: String ? = nil
for await result in Transaction.currentEntitlements {
guard let transaction = try? checkVerified (result) else { continue }
if transaction.revocationDate == nil {
current = transaction.productID
}
}
activeEntitlement = current
}
private func listenForTransactions () -> Task< Void , Never > {
Task. detached { [ weak self ] in
for await result in Transaction.updates {
guard let self ,
let transaction = try? await self . checkVerified (result)
else { continue }
await transaction. finish ()
await self . refresh ()
}
}
}
private func checkVerified < T >( _ result: VerificationResult<T>) throws -> T {
switch result {
case .unverified : throw StoreError.failedVerification
case . verified ( let safe) : return safe
}
}
}
enum StoreError : Error { case failedVerification }
ここで一番伝えたいのは、activeEntitlement を「購入ボタンを押した瞬間のフラグ」で持たないことです。権利は常に currentEntitlements から組み直す。こうしておくと、解約・返金・家族共有・別端末といった現実の揺れに強くなります。私が最初に作ったサブスクは購入時フラグで判定していて、返金されたユーザーに有料機能が残り続ける不具合を出しました。revocationDate の確認で防げます。
transaction.finish() の呼び忘れも定番のハマりどころです。finish しないとそのトランザクションは「未完了」とみなされ、起動のたびに updates に再通知されます。
無料で何を渡し、何を有料にするか
フリーミアムの設計は、機能を線で割る作業です。ここを「無料を絞れば課金する」と考えると、たいてい失敗します。私の経験では、無料で価値を実感してもらえなかったユーザーは、有料の存在にすら気づかず去っていきます。
線引きの原則を一つだけ挙げるなら、「無料で一度は成功体験を渡す」です。スケジュール最適化アプリなら、最適化を一度は丸ごと体験させる。日記アプリなら、最初の一週間は制限なく書ける。そのうえで、継続的に使いたくなった人に対して上限や同期、エクスポートといった「続けるほど効く」機能を有料側に置きます。
価格は二段構えが扱いやすいです。月額と、二ヶ月分ほど割り引いた年額。年額は解約の機会を年一回に減らすので、LTV を素直に押し上げます。三段目の高額プランは、API アクセスやチーム機能など「明確に別の顧客層」に向ける場合だけ足します。層を増やすほど選択の摩擦が増えるので、迷ったら二段で始めるのが私の好みです。
ペイウォールを「いつ」見せるか
ペイウォールは、出す内容より出すタイミングで成果が変わります。起動直後に全画面で価格を突きつけるのは、最も離脱を生む出し方です。
私が実装で使い分けているのは三つの局面です。
1. オンボーディング直後(ソフト)
- 価値を一度体験させた後に「続けるなら」と軽く提示
- スキップを必ず残す
- 想定転換: 新規の 3〜5%
2. 無料の上限に到達した瞬間(コンテキスト)
- 「いま続けたい」という意思が最も強い瞬間
- その場で解放できる即時性を見せる
- 想定転換: 上限到達者の 5〜10%
3. 数日離れたユーザーへの再訪導線(リエンゲージ)
- 通知から戻った先で、無料トライアル付きで提示
- 想定転換: 戻ってきた人の 2〜3%
数字は私の運用感に基づく目安で、ジャンルやアプリの完成度で大きく動きます。重要なのは、これを「固定の真実」として扱わず、自分のアプリで計測して置き換えることです。ペイウォール文言は価格訴求より価値訴求のほうが伸びやすい、という傾向だけは比較的安定しています。「月額1,980円」より「毎日の予定づくりを手放す」のほうが、私の手元では一貫して反応が良いものでした。
解約を「数字」ではなく「離れる理由」で見る
チャーン率を %でだけ眺めていると、打ち手が抽象的になります。私は解約を、理由のカテゴリに割って捉えるようにしています。
価値を実感しきれずに離れる人、価格に納得しきれない人、競合に移る人、不具合で嫌になった人。同じ「解約」でも、効く対策はまったく違います。価値の実感不足には最初の一週間の体験設計が効き、価格不満には年額や期間限定の感謝価格が効きます。不具合由来の解約だけは、機能追加では絶対に止まりません。クラッシュ率の監視と即時修正が唯一の答えです。
LTV の桁は、チャーン率で決まります。粗い目安ですが、月次チャーン率が10%なら平均継続は約10ヶ月、4%なら約25ヶ月です。月額1,980円・粗利7割と置けば、前者の LTV は約1.4万円、後者は約3.5万円。チャーンを半分以下に抑えるだけで、LTV は倍以上になります。新機能を一つ足すより、既存ユーザーが離れない設計のほうが、収益への効きが大きい場面は少なくありません。
ユニットエコノミクスの最低ライン
「このアプリを伸ばすべきか」を私が判断する物差しは、LTV ÷ CAC です。
CAC(顧客獲得コスト)は、広告や制作にかけた費用を、それで増えた有料会員数で割った値です。LTV が CAC を上回らなければ、回せば回すほど赤字になります。一般に LTV÷CAC が3を超えれば健全、と言われます。私はもう少し保守的に、個人開発で広告費を投じる前は最低でも3、できれば5を見たいと考えています。
ここで AdMob 時代の感覚が効いてきます。広告単価がどの国・どの時間帯で動くかを体で知っていると、無料ユーザーから得られる広告収益と、有料転換から得られる収益を同じ土俵で比較できます。無料層の広告 ARPU が月数円規模なのに対し、一人の有料会員は月1,980円。転換率が数%でも、サブスクが収益の重心になっていく理由がここにあります。
数字を作るときは、希望的な転換率を置かないことを自分に課しています。新規の有料転換は、まず5%を超えないと考えて事業計画を引く。超えたら嬉しい誤算、という構えのほうが、長く続けるうえで健全だと感じています。
App Store 審査で落ちないサブスク表示
技術が正しくても、表示で落ちることがあります。サブスクの審査は表示要件が明確で、ここを軽く見ると差し戻しで一〜二週間を失います。
最低限、ペイウォール画面に「価格・請求周期・自動更新であること・解約方法」を目立つ位置で書く。トライアルを付けるなら「初回購読者のみ・期間終了後は自動で月額課金・いつでも解約可能」まで明記する。解約導線への案内をアプリ内に置く。プライバシーポリシーと利用規約に購読条項を含める。これらは Apple のガイドライン4.8が求める実質的な要件です。
日本市場では景品表示法も意識します。通常価格と割引価格を併記するなら、その割引の条件(期間限定・キャンペーンであること)を必ず添える。「感謝価格」のような言い回しを使う場合も同じです。誠実に条件を書くことは、審査対策であると同時に、課金してくださる方への礼儀だと考えています。
次の一手
もし今、最初のサブスクを組むなら、私はまず「無料で渡す成功体験」を一つだけ言語化することから始めます。機能表ではなく、ユーザーが最初の数分で何を得るか。そこが決まれば、ペイウォールの位置も、有料側に置く機能も、自然と定まっていきます。
Rork Max は足場を驚くほど速く用意してくれますが、権利状態の正しさと価値の線引きだけは、作り手が自分の判断で握る領域です。私自身まだ最適解を探している途中ですが、広告からサブスクへ収益の形を変えていく過程で見えてきたことを、同じ道を歩く方と分かち合えたら嬉しいです。お読みいただき、ありがとうございました。