iOS SDKでFlow Builderを使ってアプリ内課金を有効にする
アプリ内課金を有効にするには、3つの重要な概念を理解する必要があります。
- プロダクト – ユーザーが購入できるもの(サブスクリプション、消耗型アイテム、永続アクセス)
- フロー – ノーコードのフロービルダーで作成した、プロダクトをユーザーに提示するための画面シーケンス。SDKは
getFlowを使って取得します。独自のコードでUIを構築したい場合は、代わりにペイウォールを使用してください — ペイウォールを手動で実装するを参照。
- プレースメント – アプリ内でフローをどこで・いつ表示するか(
main、onboarding、settingsなど)。ダッシュボードでフローをプレースメントに紐付け、コード内ではプレースメントIDで取得します。これにより、A/B テストの実施や、ユーザーごとに異なるフローの表示が簡単に行えます。
Adapty では、アプリ内での購入を有効にする方法を3つ提供しています。アプリの要件に応じて、いずれかを選択してください。
| 実装方法 | 複雑さ | 使用する場面 |
|---|---|---|
| Adapty フロービルダー | ✅ 簡単 | ノーコードビルダーで購入対応済みのフローを作成します。Adapty が自動的にレンダリングし、購入フロー・レシート検証・サブスクリプション管理をすべて裏側で処理します。 |
| 手動作成のペイウォール | 🟡 普通 | アプリコードでペイウォール UI を実装しますが、プロダクトラインナップの柔軟性を保つために Adapty からフローオブジェクトを取得します。ガイドをご覧ください。 |
| オブザーバーモード | 🔴 難しい | 独自の購入処理インフラが既にあり、それを引き続き使用したい場合に選択します。オブザーバーモードには Adapty における制限事項があります。こちらの記事をご覧ください。 |
以下の手順は、Adapty フロービルダーで作成したフローを実装する方法を説明しています。
ペイウォールの UI を自分でビルドしたい場合は、ペイウォールを手動で実装するを参照してください。
Adapty フロービルダーで作成したフローを表示するには、アプリのコードで以下の作業を行うだけです。
- フローを取得する: Adapty からフローを取得します。
- 表示する(購入処理は Adapty が担当): アプリにビューを表示します。
- ボタンアクションを処理する: ユーザーの操作に対するアプリの応答を関連付けます。たとえば、ユーザーがボタンをタップしたときにリンクを開いたり、フローを閉じたりします。
始める前に
始める前に、以下の手順を完了してください:
- Adapty ダッシュボードでアプリを App Store に接続する。
- Adapty でプロダクトを作成する。
- フローを作成してプロダクトを追加する。
- プレースメントを作成してフローを追加する。
- アプリのコードに Adapty SDK をインストールして有効化する。このガイドでは Adapty iOS SDK v4 の API を使用します。
1. フローを取得する
フローはダッシュボードで設定されたプレースメントに関連付けられています。プレースメントを使うと、オーディエンスごとに異なるフローを表示したり、A/B テストを実施したりすることができます。
Adapty フロービルダーで作成したフローを取得するには、以下の手順を行います。
getFlow メソッドを使ってプレースメント ID から flow オブジェクトを取得し、ビュー設定が含まれているか確認します。
getFlowConfiguration メソッドを使ってビュー設定を取得します。ビュー設定には、フローを表示するために必要な UI 要素とスタイリング情報が含まれています。
func loadFlow() async {
let flow = try await Adapty.getFlow(placementId: "YOUR_PLACEMENT_ID")
guard flow.hasViewConfiguration else {
print("Flow doesn't have a view configuration")
return
}
flowConfiguration = try await AdaptyUI.getFlowConfiguration(forFlow: flow)
}
2. フローを表示する
フローの設定が完了したら、あとは数行のコードを追加するだけでフローを表示できます。
SwiftUI でフローを表示する際は、イベントの処理も必要です。didFinishPurchase、didFailPurchase、didFinishRestore、didFailRestore、didReceiveError は必須です。テスト時は、以下のスニペットのコードをそのままコピーしてイベントをログに記録できます。
フローは購入が成功しても自動的に閉じません。didFinishPurchase で表示バインディングを false に設定して閉じるか、何もしないでフローを次の画面に進めることができます。
.flow(
isPresented: $flowPresented,
flowConfiguration: flowConfiguration,
didFinishPurchase: { product, purchaseResult in
print("Purchase finished successfully")
flowPresented = false // or do nothing to let the flow continue
},
didFailPurchase: { product, error in
print("Purchase failed: \(error)")
},
didFinishRestore: { profile in
print("Restore finished successfully")
},
didFailRestore: { error in
print("Restore failed: \(error)")
},
didReceiveError: { error in
flowPresented = false
print("Flow error: \(error)")
}
)
func presentFlow(with config: AdaptyUI.FlowConfiguration) {
let flowController = try AdaptyUI.flowController(
with: config,
delegate: self
)
present(flowController, animated: true)
}
AdaptyFlowControllerDelegate を実装してイベントを処理します。デフォルト実装がない4つのメソッドは最低限実装してください。購入が成功してもコントローラーは自動的に閉じないことに注意してください。didFinishPurchase で閉じるか、フローが次の画面に進むようにそのままにしてください。
extension YourViewController: AdaptyFlowControllerDelegate {
func flowController(_ controller: AdaptyFlowController,
didFinishPurchase product: AdaptyPaywallProduct,
purchaseResult: AdaptyPurchaseResult) {
if !purchaseResult.isPurchaseCancelled {
controller.dismiss(animated: true) // or do nothing to let the flow continue
}
}
func flowController(_ controller: AdaptyFlowController,
didFailPurchase product: AdaptyPaywallProduct,
error: AdaptyError) {
print("Purchase failed: \(error)")
}
func flowController(_ controller: AdaptyFlowController,
didFinishRestoreWith profile: AdaptyProfile) {
print("Restore finished successfully")
}
func flowController(_ controller: AdaptyFlowController,
didFailRestoreWith error: AdaptyError) {
print("Restore failed: \(error)")
}
}
フローの表示方法の詳細については、ガイドをご参照ください。
ユーザーがボタンをタップすると、iOS SDK は購入・復元・フローのクローズ・リンクの開閉を自動的に処理します。
ただし、カスタムまたは事前定義された ID を持つボタンについては、コード側でアクションを処理する必要があります。また、デフォルトの動作をオーバーライドしたい場合もあるでしょう。
たとえば、閉じるボタンの処理方法を以下に示します。UIKit では、.close が発火した際に SDK が自動的にコントローラーを閉じます。カスタムの動作が必要な場合のみオーバーライドしてください。SwiftUI では、isPresented バインディングを自分で false に設定する必要があります。
.flow(
isPresented: $flowPresented,
flowConfiguration: flowConfiguration,
didPerformAction: { action in
switch action {
case .close:
flowPresented = false // dismiss the flow when the user taps close
default:
break
}
},
didFinishPurchase: { product, purchaseResult in flowPresented = false },
didFailPurchase: { product, error in /* handle the error */ },
didFinishRestore: { profile in /* check access level and dismiss */ },
didFailRestore: { error in /* handle the error */ },
didReceiveError: { error in flowPresented = false }
)
extension YourViewController: AdaptyFlowControllerDelegate {
func flowController(_ controller: AdaptyFlowController,
didPerform action: AdaptyUI.Action) {
switch action {
case .close:
controller.dismiss(animated: true) // default behavior — override only if needed
default:
break
}
}
}
次のステップ
ご質問やお困りのことがあれば、サポートフォーラムをご覧ください。よくある質問への回答を見つけたり、ご自身の質問を投稿することができます。チームとコミュニティがサポートいたします!
フローをアプリで表示する準備ができました。サンドボックスモードで購入をテストして、テスト購入が正常に完了できることを確認してください。
次に、適切なユーザーにフローを表示したり有料機能へのアクセスを付与したりするために、ユーザーのアクセスレベルを確認する必要があります。
完全なコード例
このガイドのすべての手順をアプリに統合する方法を以下に示します。
struct ContentView: View {
@State private var flowPresented = false
@State private var flowConfiguration: AdaptyUI.FlowConfiguration?
@State private var isLoading = false
@State private var hasInitialized = false
var body: some View {
VStack {
if isLoading {
ProgressView("Loading...")
} else {
Text("Your App Content")
}
}
.task {
guard !hasInitialized else { return }
await initializeFlow()
hasInitialized = true
}
.flow(
isPresented: $flowPresented,
flowConfiguration: flowConfiguration,
didPerformAction: { action in
switch action {
case .close:
flowPresented = false
default:
break
}
},
didFinishPurchase: { product, purchaseResult in
print("Purchase finished successfully")
flowPresented = false // or do nothing to let the flow continue
},
didFailPurchase: { product, error in
print("Purchase failed: \(error)")
},
didFinishRestore: { profile in
print("Restore finished successfully")
},
didFailRestore: { error in
print("Restore failed: \(error)")
},
didReceiveError: { error in
print("Flow error: \(error)")
flowPresented = false
}
)
}
private func initializeFlow() async {
isLoading = true
defer { isLoading = false }
await loadFlow()
flowPresented = true
}
private func loadFlow() async {
do {
let flow = try await Adapty.getFlow(placementId: "YOUR_PLACEMENT_ID")
guard flow.hasViewConfiguration else {
print("Flow doesn't have a view configuration")
return
}
flowConfiguration = try await AdaptyUI.getFlowConfiguration(forFlow: flow)
} catch {
print("Failed to load: \(error)")
}
}
}
class ViewController: UIViewController {
private var flowConfiguration: AdaptyUI.FlowConfiguration?
override func viewDidLoad() {
super.viewDidLoad()
Task {
await initializeFlow()
}
}
private func initializeFlow() async {
do {
flowConfiguration = try await loadFlow()
if let flowConfiguration {
await MainActor.run {
presentFlow(with: flowConfiguration)
}
}
} catch {
print("Error initializing: \(error)")
}
}
private func loadFlow() async throws -> AdaptyUI.FlowConfiguration? {
let flow = try await Adapty.getFlow(placementId: "YOUR_PLACEMENT_ID")
guard flow.hasViewConfiguration else {
print("Flow doesn't have a view configuration")
return nil
}
return try await AdaptyUI.getFlowConfiguration(forFlow: flow)
}
private func presentFlow(with config: AdaptyUI.FlowConfiguration) {
guard let flowController = try? AdaptyUI.flowController(
with: config,
delegate: self
) else { return }
present(flowController, animated: true)
}
}
extension ViewController: AdaptyFlowControllerDelegate {
func flowController(_ controller: AdaptyFlowController,
didFinishPurchase product: AdaptyPaywallProduct,
purchaseResult: AdaptyPurchaseResult) {
if !purchaseResult.isPurchaseCancelled {
controller.dismiss(animated: true) // or do nothing to let the flow continue
}
}
func flowController(_ controller: AdaptyFlowController,
didFailPurchase product: AdaptyPaywallProduct,
error: AdaptyError) {
print("Purchase failed for \(product.vendorProductId): \(error)")
guard error.adaptyErrorCode != .paymentCancelled else { return }
let message = switch error.adaptyErrorCode {
case .paymentNotAllowed:
"Purchases are not allowed on this device."
default:
"Purchase failed. Please try again."
}
let alert = UIAlertController(title: "Purchase Error", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
func flowController(_ controller: AdaptyFlowController,
didFinishRestoreWith profile: AdaptyProfile) {
print("Restore finished successfully")
controller.dismiss(animated: true)
}
func flowController(_ controller: AdaptyFlowController,
didFailRestoreWith error: AdaptyError) {
print("Restore failed: \(error)")
}
func flowController(_ controller: AdaptyFlowController,
didReceiveError error: AdaptyUIError) {
print("Flow error: \(error)")
controller.dismiss(animated: true)
}
}