最近、アプリ開発を本格的に取り組んでいる中で、何度か躓いたところがあったので、同じような方のためになればと思い、まとめました。
App Store ConnectやDeveloperに課金について解説しているページもあるので、そちらも参考になされて下さい。
「買い切り」には2種類ある(先に方針を決める)
買い切りの実現方法は主に2つです。
- 有料アプリ(Paid App):ダウンロード時点で課金。
- 無料 + アプリ内課金(非消耗型 / Non-Consumable):無料で配布し、アプリ内で1回だけ課金して永久解放。
初心者の個人開発で簡単に実装できるのは ②(非消耗型 IAP)。無料で試せるのでCVが作りやすく、購入後は「復元(Restore)」で再インストール・機種変更に対応できます。Appleも「過去の購入はすぐに反映」「復元手段の提供」を明確に要求しています。(AppleDeveloper)
私の場合は、最初に無料でリリース後、バージョンアップ時に買切りを実装しました。初めての課金申請だったので、大分、時間がかかりました。
事前に、App Store Connectで支払い情報/税などを登録する必要があります。
App Store Connect で「買い切り商品(非消耗型)」を作る
商品を作成(Non-Consumable)
App Store Connect → 対象アプリ → Monetization(収益化) → In-App Purchases → 作成
タイプは Consumable ではなく Non-Consumable を選びます。
メタデータ(表示名・説明・価格)を入れる
ユーザーに見える文言は ローカライズ(表示名・説明) 側で設定します。
画像2種類に注意(ここが落とし穴)
- Image(表示用画像):1024×1024、72dpi、RGB、角丸なし…など要件があります(App Store上の露出に使われる枠)。
- App Review Screenshot(審査用スクショ):審査だけに使われ、ストアには表示されません。アプリが対応するスクリーンショット仕様に合わせてアップします。
さらに、審査を通しやすくするため Review Notes(審査メモ) に「どの画面で購入できるか」「テスト手順」を書くのが定石のようです。
実装(StoreKit 2 / SwiftUI)最小構成
ここでは「買い切り解除(productID 1つ)」の定番構成を載せます。
Store(課金管理クラス)を作る
import StoreKit
import SwiftUI
@MainActor
final class PurchaseStore: ObservableObject {
// あなたの Product ID(App Store Connect で設定したもの)
static let proProductID = "com.example.app.pro"
@Published var product: Product?
@Published var isProUnlocked: Bool = false
@Published var lastErrorMessage: String?
private var updatesTask: Task<Void, Never>?
init() {
// 1) 起動直後から Transaction.updates を監視(取りこぼし防止)
updatesTask = Task { await observeTransactionUpdates() }
// 2) 起動時に entitlement を復元(再インストール/機種変更でも反映)
Task { await refreshEntitlements() }
// 3) 商品情報を取得
Task { await loadProduct() }
}
deinit { updatesTask?.cancel() }
func loadProduct() async {
do {
let products = try await Product.products(for: [Self.proProductID])
self.product = products.first
} catch {
lastErrorMessage = "商品情報の取得に失敗: \(error.localizedDescription)"
}
}
func buyPro() async {
guard let product else { return }
do {
let result = try await product.purchase()
switch result {
case .success(let verification):
let transaction = try checkVerified(verification)
await transaction.finish() // finish は必須
await refreshEntitlements()
case .userCancelled:
break
case .pending:
// 承認待ち(Ask to Buy等)
break
@unknown default:
break
}
} catch {
lastErrorMessage = "購入に失敗: \(error.localizedDescription)"
}
}
/// 「復元」ボタンに紐付ける(非消耗型は復元導線が必要)
func restore() async {
do {
try await AppStore.sync()
await refreshEntitlements()
} catch {
lastErrorMessage = "復元に失敗: \(error.localizedDescription)"
}
}
/// 現在の権利(entitlements)から Pro 解放を判断
func refreshEntitlements() async {
var unlocked = false
for await result in Transaction.currentEntitlements {
if let transaction = try? checkVerified(result),
transaction.productID == Self.proProductID {
unlocked = true
}
}
isProUnlocked = unlocked
}
/// アプリ外で発生した取引も含め、更新を監視
private func observeTransactionUpdates() async {
for await result in Transaction.updates {
if let transaction = try? checkVerified(result),
transaction.productID == Self.proProductID {
await transaction.finish()
await refreshEntitlements()
}
}
}
private func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
switch result {
case .verified(let safe): return safe
case .unverified: throw StoreError.failedVerification
}
}
enum StoreError: Error { case failedVerification }
}
躓いたポイントとその対策です。
- 起動直後から
Transaction.updatesを監視:購入成功を取りこぼさないための対応です。 Transaction.currentEntitlementsで権利を復元:再インストールや機種変更でも「購入済み」を即反映しやすくなります。- 復元(Restore Purchases)導線が必須:非消耗型は復元ボタン等を用意する必要があります。
- 永続化の考え方:非消耗型は「レシート/取引情報」を永続記録として扱うのが基本で、ローカルのフラグはキャッシュに留めるのが安全です。
SwiftUI の画面(購入・復元ボタン)
struct PaywallView: View {
@StateObject private var store = PurchaseStore()
var body: some View {
VStack(spacing: 16) {
Text(store.isProUnlocked ? "✅ Pro 解放済み" : "🔒 Pro を解放する")
.font(.title3)
if let product = store.product {
Text("買い切り: \(product.displayPrice)")
Button("購入する") {
Task { await store.buyPro() }
}
.buttonStyle(.borderedProminent)
Button("購入を復元する") {
Task { await store.restore() }
}
.buttonStyle(.bordered)
} else {
ProgressView("商品情報を取得中…")
}
if let msg = store.lastErrorMessage {
Text(msg).foregroundStyle(.red)
}
}
.padding()
}
}
テスト(開発中に“課金実機”は不要)
ここを最優先で押さえると、後工程が一気に楽です。
4-1. Xcode の StoreKit Testing(ローカルで即テスト)
Xcode では StoreKit Configuration File を作って、App Store Connect を作り込む前でも課金フローを動かせます。
- File → New → StoreKit Configuration File

- Nameを付ける(App Store Connectと合わせる)→チェックはしない

- 「+」→「Non-consumable」を選択

- Scheme の Options でそのファイルを選択して有効化してコンパイル
あとは、アプリのテストで機能確認するだけです。
まとめ
買い切り(非消耗型IAP)は、(1) App Store Connect 商品作成→(2) StoreKit2 実装(購入・復元・権利反映)→(3) テスト(Xcode/Sandbox/TestFlight)→(4) 審査情報(Review Screenshot/Notes)の順で進めると、上手くいきます。
私の場合は、「 StoreKit Configuration File」作成時に、App Store ConnectとのSyncにチェックを入れて作成すると上手くいきませんでした。ですので、本手順ではチェックを外しております。
手順等過不足ありましたら、ご指摘下さい。随時更新していきます。
