iOSSDKの初回起動時にAA対象ペイウォールを表示する
Apple Ads (AA) のアトリビューションは Adapty.activate() の後に非同期で届きます。getPaywall を早めに呼び出すと、アトリビューションがまだ反映されていないことが多く、Adapty はデフォルトオーディエンスに基づいてプレースメントを解決してしまいます。その結果、AA セグメント向けのペイウォールが表示されません。AdaptyProfile.appliedExternalAttributionProviders を使うと、AA のアトリビューションがプロファイルに適用されたタイミングをアプリ側で検知できるため、AA セグメントが正しく解決されるまでペイウォールのリクエストを待機させることができます。
始める前に
必要なもの:
- Adapty iOS SDK 3.17.1 以降。
- Adapty でアプリに Apple Ads が設定済みであること。Apple Ads を参照してください。
以下の例では、SDK 4.1 以降のプロパティ名 appliedExternalAttributionProviders を使用しています。SDK バージョン 3.17.1〜4.0 では、プロパティ名は appliedAttributionSources です。
仕組み
Adapty.activate() を呼び出すと、SDK はバックグラウンドで Apple Ads のアトリビューションを Apple に問い合わせ、その結果を Adapty のバックエンドに転送します。AA がそのプロファイルのアクティブなアトリビューションソースになると、SDK は更新された AdaptyProfile を返します。この AdaptyProfile の appliedExternalAttributionProviders 配列には .appleAds が含まれています。
配列が空の場合、以下のいずれかを意味します:
- このプロファイルの Apple Ads アトリビューションがまだ処理されていない。
- アトリビューションがまったく届いていない。
空の配列を渡しても、getPaywall は安全に呼び出せます — Adapty は現在のプロファイル状態に一致するオーディエンス(通常はデフォルトオーディエンス)に基づいてリクエストを解決します。
この待機は初回起動時のみ適用されます。Apple Ads のアトリビューションが一度記録されると、プロファイルに永続的に保存されます。以降の起動では、キャッシュされたプロファイルにすでに appliedExternalAttributionProviders 内の .appleAds が含まれており、didLoadLatestProfile はその値とともに即座に発火し、getPaywall は遅延なく Apple Ads のセグメントに対応したペイウォールを返します。
実装
初回起動時は、プロファイル内の .appleAds を監視しながら、厳格なタイムアウトを設定してください — Apple Ads のアトリビューションが届かない場合でも、ユーザーにはペイウォールを表示する必要があります。
- SDKを有効化します。 iOS SDKのインストールと設定を参照してください。
AdaptyDelegateに準拠し、didLoadLatestProfileを実装してプロファイルの更新を購読します。 デリゲートをまだ設定していない場合は、サブスクリプションの更新を監視するを参照してください。appliedExternalAttributionProviders内の.appleAdsを監視します。 それが表示されたら、ペイウォールをリクエストします — AdaptyはAAセグメント化されたバリアントを返します。
extension <YourAdaptyDelegateImpl>: AdaptyDelegate {
nonisolated func didLoadLatestProfile(_ profile: AdaptyProfile) {
if profile.appliedExternalAttributionProviders.contains(where: { $0 == .appleAds }) {
// load paywall via Adapty.getPaywall(placementId:)
}
}
}
- 購読と並行して 3〜5 秒のタイマーを開始します。 タイマーが先に発火した場合は、
.appleAdsが現れなくてもペイウォールをリクエストします:
どちらのパスが先に発火しても、その時点でペイウォールを読み込み、もう一方のパスはスキップします。ペイウォールが 2 回フェッチされないよう、単一の状態フラグ(例:hasLoadedPaywall)を使って重複を排除してください。ネットワークリクエストが失敗してもユーザーが詰まらないよう、プレースメントに フォールバックペイウォール を設定してください。
完全なサンプル
以下の実装は、アトリビューションとタイムアウトを競争させ、デフォルトのオーディエンスのペイウォールを並行してプリフェッチし、適切なペイウォールを返します。呼び出し元は単一の非同期関数を await するだけで、呼び出し側でデリゲートや状態フラグを管理する必要はありません。
ProfileObserver は AdaptyDelegate からプロファイルの更新を公開する再利用可能なシングルトンです。PaywallLoader.getPaywallOrDefault は structured concurrency の TaskGroup を使って競争を実行します:
- タイムアウト内にアトリビューションが届いた場合、
getPaywall(placementId:)を通じてセグメント化されたペイウォールを返します。 timeoutが先に経過した場合、getPaywallForDefaultAudience(placementId:)を通じてプリフェッチ済みのデフォルトオーディエンスのペイウォールを返します。
/// Demonstrates how to fetch a paywall that depends on attribution being applied,
/// falling back to the default-audience paywall if attribution doesn't arrive in time.
///
/// Stateless and self-contained: every call kicks off its own default-audience
/// prefetch and races it against attribution + segmented fetch.
enum PaywallLoader {
static func getPaywallOrDefault(
placementId: String,
timeout: TimeInterval
) async throws -> AdaptyPaywall {
struct TimedOut: Error {}
// Kick off the default-audience request immediately so it has the full
// `timeout` window to load. We'll either cancel it on success or await
// its result on timeout — never a duplicate network call.
let defaultPaywallTask = Task {
try await Adapty.getPaywallForDefaultAudience(placementId: placementId)
}
do {
// Race two child tasks: whichever finishes first wins.
let result = try await withThrowingTaskGroup(of: AdaptyPaywall.self) { group in
// 1. Wait for attribution, then ask Adapty for the segmented paywall.
group.addTask {
await waitForAttribution()
return try await Adapty.getPaywall(placementId: placementId)
}
// 2. Time-bomb: throws `TimedOut` after `timeout` seconds.
group.addTask {
try await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000))
throw TimedOut()
}
guard let value = try await group.next() else { throw CancellationError() }
group.cancelAll() // stop the loser (sleeper or the attribution wait).
return value
}
// Segmented paywall won — we no longer need the default-audience prefetch.
defaultPaywallTask.cancel()
return result
} catch is TimedOut {
// Attribution didn't apply in time — return the prefetched default
// (instant if already done, otherwise we await the in-flight request).
return try await defaultPaywallTask.value
}
}
/// Suspends until a profile with the desired attribution source is observed.
/// `@Published.values` emits the current profile immediately on subscription,
/// so this returns on the first iteration if attribution is already applied.
@MainActor
private static func waitForAttribution() async {
for await profile in ProfileObserver.shared.$profile.values {
if profile?.appliedExternalAttributionProviders.contains(.appleAds) == true { return }
}
}
}
@MainActor
final class ProfileObserver: AdaptyDelegate {
static let shared = ProfileObserver()
@Published private(set) var profile: AdaptyProfile?
nonisolated func didLoadLatestProfile(_ profile: AdaptyProfile) {
Task { @MainActor [weak self] in
self?.profile = profile
}
}
}
Adapty.activate() の完了後、一度だけ ProfileObserver を AdaptyDelegate に接続します:
Adapty.delegate = ProfileObserver.shared
スプラッシュスクリーンから呼び出します:
do {
let paywall = try await PaywallLoader.getPaywallOrDefault(
placementId: "YOUR_PLACEMENT_ID",
timeout: 5
)
// present the paywall
} catch {
// handle the error or show a fallback paywall
}
他の目的(例:サブスクリプションのアップデートを監視する)のために既に AdaptyDelegate を使用している場合は、Adapty.delegate = ProfileObserver.shared を設定する代わりに、既存のデリゲートから ProfileObserver.shared に didLoadLatestProfile を転送してください。