首次启动时显示 Apple 广告定向付费墙

Apple Ads (AA) 归因数据在 Adapty.activate() 之后异步到达。如果过早调用 getPaywall,归因数据往往尚未到位,Adapty 会根据默认目标受众来解析版位——从而绕过你基于 AA 市场细分的付费墙。AdaptyProfile.appliedAttributionSources 让应用能够检测 AA 归因何时已应用到用户画像,以便付费墙请求等到 AA 市场细分正确解析后再发出。

开始之前

你需要:

  • Adapty iOS SDK 3.17.1 或更高版本。
  • 在 Adapty 中为应用配置 Apple Ads。请参阅 Apple Ads

工作原理

调用 Adapty.activate() 后,SDK 会在后台向 Apple 请求 Apple Ads 归因数据,并将结果转发至 Adapty 后端。当 AA 成为该用户画像的有效归因来源时,SDK 会返回一个更新后的 AdaptyProfile,其 appliedAttributionSources 数组中包含 .appleAds

数组为空可能意味着以下任一情况:

  • 该用户画像的 Apple Ads 归因尚未处理完成。
  • 归因数据尚未到达。 即使传入空数组,调用 getPaywall 也是安全的——Adapty 会根据当前用户画像状态匹配对应的目标受众来处理请求,通常为默认目标受众。

这个等待仅适用于首次启动。一旦 Apple Ads 归因数据被记录,它会永久保存在用户画像中。在后续每次启动时,缓存的用户画像已包含 .appleAds(位于 appliedAttributionSources 中),didLoadLatestProfile 会立即触发并返回该值,getPaywall 也会直接返回针对 Apple Ads 市场细分的付费墙,无需任何等待。

实现

首次启动时,监听用户画像中的 .appleAds 字段,并设置一个硬超时——即便 Apple Ads 归因数据始终未到达,这些用户也需要看到付费墙。

  1. 激活 SDK。 请参阅安装并配置 iOS SDK
  2. 订阅用户画像更新,方法是遵循 AdaptyDelegate 协议并实现 didLoadLatestProfile。如果尚未设置代理,请参阅监听订阅更新
  3. 监听 appliedAttributionSources 中的 .appleAds 一旦该值出现,立即请求付费墙——Adapty 将返回经 AA 细分的实验变体:
extension <YourAdaptyDelegateImpl>: AdaptyDelegate {
    nonisolated func didLoadLatestProfile(_ profile: AdaptyProfile) {
        if profile.appliedAttributionSources.contains(where: { $0 == .appleAds }) {
            // 通过 Adapty.getPaywall(placementId:) 加载付费墙
        }
    }
}
  1. 同步启动一个 3–5 秒的计时器。 如果计时器触发时 .appleAds 尚未出现,直接请求付费墙: 无论哪条路径先触发,都应加载付费墙;另一条路径应被跳过。使用一个状态标志(例如 hasLoadedPaywall)进行去重,避免付费墙被重复获取。为该版位配置备用付费墙,确保网络请求失败时用户不会陷入等待。

完整示例

以下实现将归因与超时进行竞争,同时预取默认受众的付费墙,并返回合适的付费墙。调用方只需等待一个异步函数——无需在调用处管理代理或状态标志。

ProfileObserver 是一个可复用的单例,用于发布来自 AdaptyDelegate 的用户画像更新。PaywallLoader.getPaywallOrDefault 使用结构化并发 TaskGroup 执行竞争逻辑:

  • 如果归因数据在 timeout 时间内到达,则通过 getPaywall(placementId:) 返回按目标受众细分的付费墙。
  • 如果 timeout 先超时,则通过 getPaywallForDefaultAudience(placementId:) 返回预取的默认受众付费墙。

/// 演示如何获取一个依赖归因数据的付费墙,
/// 若归因数据未能及时到达,则回退到默认受众付费墙。
///
/// 无状态且自包含:每次调用都会发起独立的默认受众预请求,
/// 并与"等待归因 + 获取细分付费墙"的流程竞速。
enum PaywallLoader {
    static func getPaywallOrDefault(
        placementId: String,
        timeout: TimeInterval
    ) async throws -> AdaptyPaywall {
        struct TimedOut: Error {}

        // 立即发起默认受众请求,使其拥有完整的 `timeout` 窗口来完成加载。
        // 若细分付费墙成功获取则取消它,若超时则等待其结果——绝不发起重复的网络请求。
        let defaultPaywallTask = Task {
            try await Adapty.getPaywallForDefaultAudience(placementId: placementId)
        }

        do {
            // 让两个子任务竞速,谁先完成谁赢。
            let result = try await withThrowingTaskGroup(of: AdaptyPaywall.self) { group in
                // 1. 等待归因数据就绪,然后向 Adapty 请求细分付费墙。
                group.addTask {
                    await waitForAttribution()
                    return try await Adapty.getPaywall(placementId: placementId)
                }
                // 2. 定时炸弹:经过 `timeout` 秒后抛出 `TimedOut`。
                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() // 取消落败方(定时器或归因等待任务)。
                return value
            }
            // 细分付费墙胜出——默认受众预请求不再需要,直接取消。
            defaultPaywallTask.cancel()
            return result
        } catch is TimedOut {
            // 归因数据未能在规定时间内就绪——返回预请求的默认付费墙
            // (若已完成则立即返回,否则等待进行中的请求)。
            return try await defaultPaywallTask.value
        }
    }

    /// 挂起协程,直到观察到包含所需归因来源的用户画像。
    /// `@Published.values` 在订阅时会立即发出当前值,
    /// 因此若归因已就绪,第一次迭代时即可返回。
    @MainActor
    private static func waitForAttribution() async {
        for await profile in ProfileObserver.shared.$profile.values {
            if profile?.appliedAttributionSources.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 直接设置,而是在现有的 delegate 中将 didLoadLatestProfile 转发给 ProfileObserver.shared