Показ пейвола с таргетингом AA при первом запуске
Apple Ads (AA) атрибуция поступает асинхронно после вызова Adapty.activate(). Если вы вызываете getPaywall слишком рано, атрибуция зачастую ещё не применена, и Adapty определяет плейсмент по аудитории по умолчанию — минуя ваши пейволы, сегментированные по AA. AdaptyProfile.appliedAttributionSources позволяет приложению определить момент, когда AA-атрибуция уже применена к профилю, чтобы запрос пейвола дождался корректного разрешения AA-сегментации.
Прежде чем начать
Вам потребуется:
- Adapty iOS SDK версии 3.17.1 или выше.
- Apple Ads, настроенный для приложения в Adapty. См. 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 так и не придёт, эти пользователи всё равно должны увидеть пейвол.
- Активируйте SDK. См. Установка и настройка iOS SDK.
- Подпишитесь на обновления профиля, реализовав протокол
AdaptyDelegateи методdidLoadLatestProfile. Если делегат ещё не настроен, см. Отслеживание обновлений подписки. - Следите за
.appleAdsвappliedAttributionSources. Когда оно появится, запросите пейвол — Adapty вернёт вариант, сегментированный по Apple Ads:
extension <YourAdaptyDelegateImpl>: AdaptyDelegate {
nonisolated func didLoadLatestProfile(_ profile: AdaptyProfile) {
if profile.appliedAttributionSources.contains(where: { $0 == .appleAds }) {
// load paywall via Adapty.getPaywall(placementId:)
}
}
}
- Запустите таймер на 3–5 секунд параллельно с подпиской. Если таймер сработает раньше, чем появится
.appleAds, всё равно запросите пейвол: Whichever path fires first should load the paywall; the other path should be skipped. Use a single state flag (for example,hasLoadedPaywall) to deduplicate so the paywall isn’t fetched twice. Configure a резервный пейвол for the placement so the user is never stuck if the network request fails.
Полный пример
Реализация ниже запускает гонку между атрибуцией и таймаутом, параллельно предзагружает пейвол для аудитории по умолчанию и возвращает подходящий пейвол. Вызывающий код ожидает единственную асинхронную функцию — никаких делегатов и флагов состояния на стороне вызова.
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. Таймер: выбрасывает `TimedOut` через `timeout` секунд.
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
}
}
}
Подключите ProfileObserver к AdaptyDelegate один раз, после того как Adapty.activate() завершится:
Adapty.delegate = ProfileObserver.shared
Вызовите с экрана-заставки:
do {
let paywall = try await PaywallLoader.getPaywallOrDefault(
placementId: "YOUR_PLACEMENT_ID",
timeout: 5
)
// показать пейвол
} catch {
// обработать ошибку или показать резервный пейвол
}
Если в вашем приложении уже используется AdaptyDelegate для других целей (например, для отслеживания обновлений подписки), передавайте didLoadLatestProfile в ProfileObserver.shared из существующего делегата вместо того, чтобы устанавливать Adapty.delegate = ProfileObserver.shared.