Afficher un paywall ciblé par Apple Ads au premier lancement dans iOS SDK
L’attribution Apple Ads (AA) arrive de manière asynchrone après Adapty.activate(). Si vous appelez getPaywall trop tôt, l’attribution n’est souvent pas encore disponible et Adapty résout le placement sur l’audience par défaut — en contournant vos paywalls segmentés par AA. AdaptyProfile.appliedExternalAttributionProviders permet à l’application de détecter quand l’attribution AA a été appliquée au profil, afin que la requête de paywall puisse attendre que la segmentation AA soit correctement résolue.
Avant de commencer
Vous avez besoin de :
- SDK Adapty iOS 3.17.1 ou version ultérieure.
- Apple Ads configuré pour l’application dans Adapty. Voir Apple Ads.
Les exemples ci-dessous utilisent le nom de propriété SDK 4.1+ appliedExternalAttributionProviders. Dans les versions SDK 3.17.1–4.0, la propriété se nomme appliedAttributionSources.
Comment ça fonctionne
Après Adapty.activate(), le SDK demande en arrière-plan les données d’attribution Apple Ads à Apple, puis transmet le résultat au backend d’Adapty. Lorsqu’AA devient la source d’attribution active pour le profil, le SDK délivre un AdaptyProfile mis à jour dont le tableau appliedExternalAttributionProviders contient .appleAds.
Un tableau vide peut signifier l’une des choses suivantes :
- L’attribution Apple Ads n’a pas encore été traitée pour ce profil.
- Aucune attribution n’est arrivée du tout.
Même avec un tableau vide, getPaywall reste sûr à appeler — Adapty résout la requête en fonction de l’audience qui correspond à l’état actuel du profil, généralement l’audience par défaut.
L’attente s’applique uniquement au premier lancement. Une fois l’attribution Apple Ads enregistrée, elle est stockée définitivement sur le profil. À chaque lancement ultérieur, le profil en cache contient déjà .appleAds dans appliedExternalAttributionProviders, didLoadLatestProfile se déclenche immédiatement avec cette valeur, et getPaywall retourne le paywall segmenté par Apple Ads sans aucun délai.
Implémentation
Au premier lancement, surveillez .appleAds dans le profil et appliquez un délai d’attente strict — si l’attribution Apple Ads n’arrive jamais, ces utilisateurs doivent tout de même voir un paywall.
- Activez le SDK. Voir Installer et configurer le SDK iOS.
- Abonnez-vous aux mises à jour du profil en vous conformant à
AdaptyDelegateet en implémentantdidLoadLatestProfile. Si vous n’avez pas encore configuré le délégué, voir Écouter les mises à jour d’abonnement. - Surveillez
.appleAdsdansappliedExternalAttributionProviders. Quand il apparaît, faites une requête pour le paywall — Adapty retournera la variante segmentée AA :
extension <YourAdaptyDelegateImpl>: AdaptyDelegate {
nonisolated func didLoadLatestProfile(_ profile: AdaptyProfile) {
if profile.appliedExternalAttributionProviders.contains(where: { $0 == .appleAds }) {
// load paywall via Adapty.getPaywall(placementId:)
}
}
}
- Lancez un minuteur de 3 à 5 secondes en parallèle de l’abonnement. Si le minuteur se déclenche avant l’apparition de
.appleAds, demandez le paywall quand même :
Quel que soit le chemin qui s’exécute en premier, c’est lui qui doit charger le paywall ; l’autre chemin doit être ignoré. Utilisez un seul indicateur d’état (par exemple, hasLoadedPaywall) pour éviter les doublons et ne pas récupérer le paywall deux fois. Configurez un paywall de secours pour le placement afin que l’utilisateur ne soit jamais bloqué en cas d’échec de la requête réseau.
Exemple complet
L’implémentation ci-dessous fait la course entre l’attribution et un délai d’expiration, précharge en parallèle le paywall de l’audience par défaut, et retourne le paywall approprié. L’appelant attend une seule fonction async — aucun délégué ni indicateur d’état à gérer côté appelant.
ProfileObserver est un singleton réutilisable qui publie les mises à jour de profil depuis AdaptyDelegate. PaywallLoader.getPaywallOrDefault effectue la course à l’aide d’un TaskGroup à concurrence structurée :
- Si l’attribution arrive avant
timeout, elle retourne le paywall segmenté viagetPaywall(placementId:). - Si
timeoutexpire en premier, elle retourne le paywall de l’audience par défaut préchargé viagetPaywallForDefaultAudience(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
}
}
}
Connectez ProfileObserver à AdaptyDelegate une seule fois, après la fin de Adapty.activate() :
Adapty.delegate = ProfileObserver.shared
Appelez depuis l’écran de démarrage :
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
}
Si votre application utilise déjà un AdaptyDelegate à d’autres fins (par exemple, écouter les mises à jour d’abonnement), transmettez didLoadLatestProfile à ProfileObserver.shared depuis votre délégué existant plutôt que de définir Adapty.delegate = ProfileObserver.shared.