Important
All ATT lifecycle handling belongs to the host app. automaticHandlingOfAtt is being deprecated and will be removed from the SDK. Keep it disabled and use the fully manual flow described on this page. Never show the ATT system alert and the iubenda consent layer at the same time, or one on top of the other.
App Tracking Transparency (ATT) and the iubenda CMP consent flow are separate systems that you need to coordinate.
- ATT is Apple’s permission for cross-app and cross-website tracking, including access to the IDFA.
- The iubenda CMP collects the choices privacy laws require for purposes such as analytics, personalization, measurement, advertising, and app functionality.
An ATT denial does not replace the CMP flow. A user who selects Ask App Not to Track may still need to choose whether other SDKs and processing activities are allowed under applicable privacy law.
Do not initialize advertising or analytics SDKs before the required ATT and CMP choices are available. Initialize the iubenda CMP itself at launch, but present its consent UI only after ATT has been handled and the user’s ATT choice is available.
Step 1: Prerequisites
- The iubenda CMP supports iOS 12 and later.
- ATT is available on iOS 14 and later.
- Add
AppTrackingTransparency.frameworkto the app target. - Add
NSUserTrackingUsageDescriptionto the app’sInfo.plist.
<key>NSUserTrackingUsageDescription</key>
<string>This identifier will be used to deliver personalized ads and improve your experience.</string>
The text must accurately explain how your app uses tracking data. Apple may reject vague or misleading wording.
Import the required modules:
import UIKit
import AppTrackingTransparency
import iubenda
Initialize the iubenda CMP during app startup:
let config = IubendaCMPConfiguration()
config.gdprEnabled = true
config.siteId = "YOUR_SITE_ID"
config.cookiePolicyId = "YOUR_COOKIE_POLICY_ID"
config.jsonFile = Bundle.main.path(forResource: "config", ofType: "json")
config.automaticHandlingOfAtt = false
IubendaCMP.initialize(with: config)
automaticHandlingOfAtt defaults to false. Do not enable it. The app must request ATT, process every result, and then launch the CMP.
Step 2: Check ATT status
Check the current ATT status before initializing any SDK that performs tracking:
private enum AppATTStatus: Equatable {
case notApplicable
case notDetermined
case restricted
case denied
case authorized
}
private func currentATTStatus() -> AppATTStatus {
guard #available(iOS 14, *) else {
return .notApplicable
}
return mapATTStatus(ATTrackingManager.trackingAuthorizationStatus)
}
@available(iOS 14, *)
private func mapATTStatus(
_ status: ATTrackingManager.AuthorizationStatus
) -> AppATTStatus {
switch status {
case .notDetermined:
return .notDetermined
case .restricted:
return .restricted
case .denied:
return .denied
case .authorized:
return .authorized
@unknown default:
return .denied
}
}
The native ATT statuses are:
.authorized: tracking and IDFA access are permitted, subject to the user’s CMP choices..denied: tracking is not permitted..restricted: tracking is unavailable, for example because of parental controls or device management..notDetermined: the app has not requested ATT permission yet.
Heads up
Use ATTrackingManager directly. Do not use the SDK’s requestAttConsent(from:callback:), getATTStatus(from:callback:), or automaticHandlingOfAtt. These APIs are being deprecated as ATT responsibility moves entirely to the host app.
Step 3: Request ATT authorization
Request ATT only while the app is active and only when the status is .notDetermined:
private func startPrivacyFlow() {
guard UIApplication.shared.applicationState == .active else {
return
}
guard !didStartPrivacyFlow else {
return
}
didStartPrivacyFlow = true
guard #available(iOS 14, *) else {
openCMP(attStatus: .notApplicable)
return
}
let status = ATTrackingManager.trackingAuthorizationStatus
guard status == .notDetermined else {
handleATTStatus(mapATTStatus(status))
return
}
ATTrackingManager.requestTrackingAuthorization { [weak self] status in
DispatchQueue.main.async {
guard let self = self else {
return
}
self.handleATTStatus(self.mapATTStatus(status))
}
}
}
Call startPrivacyFlow() from a visible view controller and retry when the app becomes active. The method guards against repeated execution:
private var didStartPrivacyFlow = false
override func viewDidLoad() {
super.viewDidLoad()
observeConsentChanges()
NotificationCenter.default.addObserver(
self,
selector: #selector(appDidBecomeActive),
name: UIApplication.didBecomeActiveNotification,
object: nil
)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
startPrivacyFlow()
}
@objc private func appDidBecomeActive() {
startPrivacyFlow()
}
Step 4: Handle the ATT result
Route every resolved ATT state to the CMP flow. ATT denial must block tracking, but it must not be treated as a rejection of every independent CMP purpose:
private func handleATTStatus(_ status: AppATTStatus) {
switch status {
case .authorized:
openCMP(attStatus: status)
case .denied, .restricted:
stopTrackingSDKs()
// Decide before any background setStorePreferences call. A successful background
// save can mark a preference as stored and make needsCMPConsent() false afterward.
let shouldOpenCMP = needsCMPConsent()
// Never open the CMP only inside the setStorePreferences callback. That callback
// depends on a headless WebView and may not run until a CMP layer is shown (or at all
// on failure). Run optional purpose mapping in parallel; open the CMP from here.
rejectAdvertisingPurposesIfRequired { [weak self] in
guard let self = self else { return }
self.applySDKPermissions(attStatus: status)
}
if shouldOpenCMP {
IubendaCMP.openPreferences(from: self)
} else {
applySDKPermissions(attStatus: status)
}
case .notDetermined:
// Do not initialize trackers. Retry only when the app is active.
break
case .notApplicable:
openCMP(attStatus: status)
}
}
Step 5: Launch the iubenda CMP
Use IubendaCMP.askConsent(from:) for the normal consent flow.
Current SDK behavior makes askConsent(from:) a no-op while the native ATT status is .denied or .restricted. In those states, use IubendaCMP.openPreferences(from:) when a valid CMP preference is not already stored:
private func needsCMPConsent() -> Bool {
let hasStoredPreference = IubendaCMP.canOpenPreferences()
|| IubendaCMP.isPreferenceExpressed()
return !hasStoredPreference
|| IubendaCMP.shouldGetConsentForInvalidateConsentBefore()
|| IubendaCMP.shouldGetConsentForInvalidateConsentInterval()
|| IubendaCMP.shouldGetConsentForGoogleAdditionalConsentMode()
}
private func openCMP(
attStatus: AppATTStatus
) {
guard needsCMPConsent() else {
applySDKPermissions(attStatus: attStatus)
return
}
if attStatus == .denied || attStatus == .restricted {
IubendaCMP.openPreferences(from: self)
} else {
IubendaCMP.askConsent(from: self)
}
}
Unlike some CMP SDKs, this SDK has no setATTStatus property. Pass the native ATT status through your own privacy coordinator and combine it with CMP preferences when deciding which SDKs to enable.
Step 6: ATT and CMP decision table
| ATT status | IDFA | CMP behavior | SDK behavior |
|---|---|---|---|
.notDetermined |
No | Wait for ATT to resolve, then open the CMP. | Do not initialize trackers. |
.authorized |
Yes | Collect granular CMP choices with askConsent(from:). |
Enable a tracker only when its CMP purposes are also allowed. |
.denied |
No | If needed, collect CMP choices with openPreferences(from:). |
Block cross-app tracking and IDFA use. Evaluate non-tracking SDKs from CMP choices. |
.restricted |
No | Same CMP handling as .denied. |
Block cross-app tracking and IDFA use. |
| iOS earlier than 14 | N/A | Skip ATT and use the normal CMP flow. | Gate SDKs using CMP choices. |
ATT authorization is necessary but not sufficient for tracking. For example, .authorized does not allow an analytics or advertising SDK when the corresponding CMP purpose was rejected.
Step 7: ATT-aware API recipes
Reject configured advertising purposes after ATT denial (optional)
Recommended default: do not map ATT denial to CMP purposes. Block IDFA and tracking SDKs in your app when ATT is .denied or .restricted, and collect legal CMP choices through openPreferences(from:) when needed. ATT and CMP cover different scopes.
Only enable purpose mapping when legal, product, and CMP configuration explicitly require it.
Do not use this on a fresh install
setStorePreferences persists consent in a background WebView. On first launch, getStorePreferences() can carry default fields (for example an empty tcfv2 string) that must not be sent back with a partial purposes map. Doing so can store a full TCF choice without the user ever seeing the CMP, so needsCMPConsent() becomes false and the banner is skipped.
Apply purpose mapping only when the user already has stored CMP data:
private func shouldApplyPurposeRejectionOnATTDenial() -> Bool {
IubendaCMP.isPreferenceExpressed() || IubendaCMP.canOpenPreferences()
}
Build a minimal payload. Do not start from getStorePreferences() on first launch, and omit tcfv2 and gac unless you intentionally set them:
private let advertisingPurposeIDs = [4, 5] // Example IDs only - use your configuration
/// Set to true only when legal/product requires mapping ATT denial to CMP purposes.
private let mapATTDenialToAdvertisingPurposes = false
private func buildRejectionOnlyStorePreferences() -> StorePreferences {
let preferences = StorePreferences()
preferences.consent = false
preferences.tcfv2 = nil
preferences.gac = nil
var purposes: [String: Bool] = [:]
for purposeID in advertisingPurposeIDs {
purposes[String(purposeID)] = false
}
preferences.purposes = purposes
return preferences
}
private func rejectAdvertisingPurposesIfRequired(
completion: @escaping () -> Void
) {
guard mapATTDenialToAdvertisingPurposes,
shouldApplyPurposeRejectionOnATTDenial() else {
completion()
return
}
// setStorePreferences uses a headless WebView. The callback may be delayed or never
// called if the WebView fails (network, WebKit). Do not block the privacy flow on it.
var didCallCompletion = false
let finish: () -> Void = {
guard !didCallCompletion else { return }
didCallCompletion = true
DispatchQueue.main.async { completion() }
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0, execute: finish)
IubendaCMP.setStorePreferences(
storePreferences: buildRejectionOnlyStorePreferences()
) { _ in
finish()
}
}
Purpose IDs are configuration-specific. The SDK has no rejectPurposes(_:updateVendor:) equivalent, and this recipe does not update vendor choices.
Wire rejectAdvertisingPurposesIfRequired as in Step 4: optional background work in parallel with opening the CMP, not as a gate before openPreferences(from:).
Optional pre-grant after ATT authorization
Do not treat ATT authorization as legal consent by default. Only pre-grant configured purposes if your legal basis, CMP configuration, and user flow explicitly permit it.
Use the same minimal StorePreferences builder pattern (explicit purpose map, and do not rely on getStorePreferences() defaults on first launch). Do not call openCMP(attStatus:) only inside the setStorePreferences callback. Use the same bounded finish() pattern as above, or open the CMP from Step 4 without waiting on the callback.
React to CMP choices
Listen for CMP changes:
private func observeConsentChanges() {
NotificationCenter.default.addObserver(
self,
selector: #selector(consentDidChange),
name: NSNotification.Name.ConsentChanged,
object: nil
)
}
@objc private func consentDidChange() {
applySDKPermissions(attStatus: currentATTStatus())
}
Register this observer before starting the privacy flow, as shown in viewDidLoad() in Step 3.
Combine ATT with the user’s CMP purpose choices:
private func applySDKPermissions(
attStatus: AppATTStatus
) {
let attAllowsTracking = attStatus == .authorized
|| attStatus == .notApplicable
// Replace these IDs with purpose IDs from your iubenda configuration.
let analyticsAllowed = IubendaCMP.isPurposeEnabled(id: 3)
let advertisingAllowed = IubendaCMP.isPurposeEnabled(id: 4)
if analyticsAllowed {
initializeNonTrackingAnalyticsIfNeeded()
} else {
disableAnalytics()
}
if attAllowsTracking && advertisingAllowed {
initializeAdvertisingSDKIfNeeded()
} else {
disableAdvertisingAndIDFAUse()
}
}
Purpose IDs are configuration-specific. Do not copy the example IDs without checking your iubenda setup.
Do not automatically map ATT denial to rejection of every advertising, analytics, or vendor preference. ATT and CMP choices cover different scopes.
Step 8: Re-check ATT when the app resumes
Users can change ATT permission in Settings. Re-check it whenever the app becomes active:
func applicationDidBecomeActive(_ application: UIApplication) {
applySDKPermissions(attStatus: currentATTStatus())
}
For scene-based apps, perform the same check in the scene delegate:
func sceneDidBecomeActive(_ scene: UIScene) {
applySDKPermissions(attStatus: currentATTStatus())
}
Do not call requestTrackingAuthorization repeatedly. iOS displays the system alert at most once per installation, and later changes happen in Settings.
Read ATTrackingManager.trackingAuthorizationStatus directly. Do not use the deprecated SDK ATT helpers to request or re-check status.
When the user revokes ATT in Settings, update trackers with applySDKPermissions only. Do not clear existing CMP choices solely because ATT changed (see Step 9). If you use the optional purpose mapping from Step 7, a pending setStorePreferences operation may remain queued until a CMP WebView completes. Prefer skipping background purpose mapping on resume, or present openPreferences(from:) once if you must flush a queued store operation.
Step 9: Edge cases and fallbacks
- Restricted: treat tracking as unavailable, but continue the CMP flow for independent purposes.
- iOS earlier than 14: skip ATT and open the CMP.
- App inactive: wait until the app is active before requesting ATT.
- ATT denied: never access IDFA or initialize SDK features that perform tracking.
- CMP rejected: keep affected SDKs disabled even when ATT is authorized.
- No network:
askConsent(from:)may not show the CMP whenskipNoticeWhenOfflineis enabled. Retry when connectivity returns. - Existing users: do not erase a user’s CMP choices solely because ATT is denied. Re-evaluate trackers from both current states.
- Unknown future ATT status: fail closed for tracking and continue collecting any independently required CMP choices.
setStorePreferences: runs in a background WebView and sets a pending task until consent is saved. Failures or WebKit issues can delay or prevent the callback, so do not chain presenting the CMP solely on that callback.- Fresh install and ATT denied: block tracking in app code, and use
openPreferences(from:)when the CMP is required. Avoid Step 7 purpose mapping until the user has stored CMP preferences.
Step 10: Testing and debugging
- Use a fresh installation to test the first ATT prompt.
- Test
.authorized,.denied,.restricted, and.notDetermined. - Test iOS earlier than 14 if your app still supports it.
- Change ATT under Settings > Privacy & Security > Tracking.
- Confirm the CMP never appears on top of the ATT alert.
- Confirm no tracking SDK starts before both required choices resolve.
- Confirm ATT denial does not prevent the CMP from collecting choices for non-tracking purposes.
- Confirm CMP rejection keeps affected SDKs disabled even when ATT is authorized.
- Confirm reopening the CMP with
IubendaCMP.openPreferences(from:)updates SDK behavior. - Observe
NSNotification.Name.ConsentChangedand log both ATT and CMP states during development. - Fresh install, ATT denied: deny ATT without Step 7 mapping, then confirm the CMP can still open when required and that no TCF or consent string is stored without user interaction.
- Fresh install, ATT denied, with Step 7 mapping enabled: confirm you do not get a stored full TCF consent without the user seeing the CMP. Use this as a regression test if you enable mapping.
setStorePreferencescallback: with airplane mode or a slow network, confirm the privacy flow still opens the CMP when Step 4 does not wait on the callback.- Settings ATT revoke: after
.authorized, revoke tracking and confirm trackers stay off and existing CMP choices remain unless the user changes them in the CMP.
Resetting the simulator’s privacy settings or reinstalling the app may be required to return ATT to .notDetermined.
Step 11: Best practices
- Never initialize trackers before both ATT and CMP requirements are satisfied.
- Keep all ATT lifecycle handling in the host app.
- Never overlap the ATT system alert and the iubenda CMP UI.
- Use pre-prompts carefully and never mislead users.
- Treat ATT authorization as necessary but not sufficient for tracking.
- Do not infer CMP consent from ATT authorization or rejection.
- Allow users to reopen CMP preferences at any time.
- Re-check ATT when the app resumes.
- Expect App Review rejection if tracking occurs before ATT permission.
Step 12: Developer checklist
- Add
AppTrackingTransparency.framework. - Add an accurate
NSUserTrackingUsageDescription. - Initialize
IubendaCMPwithout initializing tracking SDKs. - Keep
automaticHandlingOfAtt = false, and do not use the deprecated SDK ATT helpers. - Request ATT only when the app is active and the status is
.notDetermined. - Open the CMP only after ATT resolves.
- Default: do not map ATT denial to CMP purposes. Gate tracking with ATT and legal choices with the CMP.
- If required, reject only explicitly mapped advertising purposes after ATT denial, and only when stored CMP preferences already exist. Never use
getStorePreferences()alone on first launch. - Do not present the CMP only inside a
setStorePreferencescompletion handler. - Use
openPreferences(from:)when CMP consent is needed while ATT is denied or restricted. - Observe
NSNotification.Name.ConsentChanged. - Enable each SDK only when its ATT and CMP requirements are both satisfied.
- Re-check ATT on app resume without clearing CMP preferences.
- Provide a way to reopen CMP preferences.
- Test every ATT and CMP combination.