iOSSDKの初回起動時にAA対象ペイウォールを表示する

Apple Ads (AA) のアトリビューションは Adapty.activate() の後に非同期で届きます。早い段階で getFlow を呼び出すと、アトリビューションがまだ反映されていないことが多く、Adapty はデフォルトのオーディエンスに対してプレースメントを解決してしまい、AA セグメント対象のペイウォールがスキップされます。AdaptyProfile.appliedExternalAttributionProviders を使えば、AA アトリビューションがプロファイルに適用されたタイミングをアプリ側で検知できるため、AA セグメントが正しく解決されるまでペイウォールのリクエストを待機させることができます。

Important

appliedExternalAttributionProviders はApple Adsのみを報告します。他のプロバイダーからのアトリビューションはユーザープロファイルで確認でき、セグメントフィルターでも利用できますが、ここにはまだ表示されません。

始める前に

必要なもの:

  • Adapty iOS SDK 4.1 以降。
  • アプリに Apple Ads が設定されていること(Adapty 内)。Apple Ads を参照してください。
Note

例では SDK 4.1 の API 名を使用しています。SDK 4.0 では、プロファイルのプロパティ名は appliedAttributionSources です。SDK 3.x では、同プロパティは同じ旧名称であり、ペイウォールは getPaywall/getPaywallForDefaultAudience で取得し、AdaptyPaywall を返します。Adapty iOS SDK を v4.1 へ移行する を参照してください。

仕組み

Adapty.activate() を呼び出すと、SDKはバックグラウンドでApple Adsのアトリビューション情報をAppleにリクエストし、その結果をAdaptyのバックエンドに転送します。Apple Adsがプロファイルのアクティブなアトリビューションソースになると、SDKは更新された AdaptyProfile を配信します。この AdaptyProfileappliedExternalAttributionProviders 配列には .appleAds が含まれています。

配列が空の場合、以下のいずれかを意味します:

  • このプロファイルに対してApple Adsのアトリビューションがまだ処理されていない。
  • アトリビューション情報がまったく届いていない。
  • アトリビューションが別のプロバイダーから届いたが、その情報はこの配列には含まれない。

空の配列を渡しても getFlow は安全に呼び出せます — Adapty は現在のプロファイル状態にマッチするオーディエンス(通常はデフォルトオーディエンス)に対してリクエストを解決します。

Important

この待機処理は初回起動時のみ適用されます。Apple Ads のアトリビューションが一度記録されると、プロファイルに永続的に保存されます。それ以降の起動では、キャッシュ済みのプロファイルに既に appliedExternalAttributionProviders.appleAds が含まれているため、didLoadLatestProfile はその値を即座に発火し、getFlow は遅延なく Apple Ads セグメント向けのペイウォールを返します。

実装

初回起動時は、プロファイル内の .appleAds を監視しながら、厳格なタイムアウトを設定してください — Apple Ads のアトリビューションが届かない場合でも、ユーザーにはペイウォールを表示する必要があります。

  1. SDKを有効化します。 iOS SDKのインストールと設定を参照してください。
  2. AdaptyDelegateに準拠し、didLoadLatestProfileを実装してプロファイルの更新を購読します。 デリゲートをまだ設定していない場合は、サブスクリプションの更新を監視するを参照してください。
  3. appliedExternalAttributionProviders内の.appleAdsを監視します。 それが表示されたら、ペイウォールをリクエストします — AdaptyはAAセグメント化されたバリアントを返します。
extension <YourAdaptyDelegateImpl>: AdaptyDelegate {
    nonisolated func didLoadLatestProfile(_ profile: AdaptyProfile) {
        if profile.appliedExternalAttributionProviders.contains(where: { $0 == .appleAds }) {
            // load the paywall via Adapty.getFlow(placementId:)
        }
    }
}
  1. 購読と並行して 3〜5 秒のタイマーを開始します。 タイマーが先に発火した場合は、.appleAds が現れなくてもペイウォールをリクエストします:

どちらのパスが先に発火しても、その時点でペイウォールを読み込み、もう一方のパスはスキップします。ペイウォールが 2 回フェッチされないよう、単一の状態フラグ(例:hasLoadedPaywall)を使って重複を排除してください。ネットワークリクエストが失敗してもユーザーが詰まらないよう、プレースメントに フォールバックペイウォール を設定してください。

完全な実装例

以下の実装では、アトリビューションとタイムアウトをレースさせ、デフォルトオーディエンスのペイウォールを並行してプリフェッチし、適切なペイウォールを返します。呼び出し元は単一の非同期関数を await するだけで、コールサイトでデリゲートや状態フラグを管理する必要はありません。

ProfileObserverAdaptyDelegate からのプロファイル更新を公開する再利用可能なシングルトンです。FlowLoader.getFlowOrDefault は、構造化並行処理の TaskGroup を使ってレース処理を実行します。

  • アトリビューションが timeout 内に取得できた場合は、getFlow(placementId:) でセグメント化されたペイウォールを返します。
  • timeout が先に経過した場合は、getFlowForDefaultAudience(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 FlowLoader {
    static func getFlowOrDefault(
        placementId: String,
        timeout: TimeInterval
    ) async throws -> AdaptyFlow {
        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 defaultFlowTask = Task {
            try await Adapty.getFlowForDefaultAudience(placementId: placementId)
        }

        do {
            // Race two child tasks: whichever finishes first wins.
            let result = try await withThrowingTaskGroup(of: AdaptyFlow.self) { group in
                // 1. Wait for attribution, then ask Adapty for the segmented paywall.
                group.addTask {
                    await waitForAttribution()
                    return try await Adapty.getFlow(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.
            defaultFlowTask.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 defaultFlowTask.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() の完了後、一度だけ ProfileObserverAdaptyDelegate に接続します:

Adapty.delegate = ProfileObserver.shared

スプラッシュスクリーンから呼び出します:

do {
    let flow = try await FlowLoader.getFlowOrDefault(
        placementId: "YOUR_PLACEMENT_ID",
        timeout: 5
    )
    // present the paywall
} catch {
    // handle the error or show a fallback paywall
}

他の目的(例:サブスクリプションのアップデートを監視する)のために既に AdaptyDelegate を使用している場合は、Adapty.delegate = ProfileObserver.shared を設定する代わりに、既存のデリゲートから ProfileObserver.shareddidLoadLatestProfile を転送してください。