Implementing App Tracking Transparency (ATT) for iOS
Starting with iOS 14.5, applications must request user permission through ATTrackingManager before accessing IDFA. Without permission, ASIdentifierManager.shared().advertisingIdentifier returns zeros (00000000-0000-0000-0000-000000000000). An application without proper ATT implementation receives a rejection during review under guideline 5.1.2, and advertising networks receive zero attribution data.
What exactly needs to be done
Info.plist: The NSUserTrackingUsageDescription key is mandatory. Without it, the application crashes with an exception when calling requestTrackingAuthorization. The text must be specific — Apple rejects submissions with generic formulations like "to improve experience". A working example: "We use device data to show relevant advertisements and measure advertising campaign effectiveness".
Timing of display. requestTrackingAuthorization can only be called once — subsequent calls don't show the dialog but immediately return the cached status. Therefore, timing is important: show it after onboarding, not at cold start. A user who doesn't understand why access is needed will tap "Ask App Not to Track".
import AppTrackingTransparency
import AdSupport
func requestTrackingPermission() async {
// Wait for the application to become active — otherwise the dialog won't appear
await MainActor.run {
guard UIApplication.shared.applicationState == .active else { return }
}
let status = await ATTrackingManager.requestTrackingAuthorization()
switch status {
case .authorized:
let idfa = ASIdentifierManager.shared().advertisingIdentifier.uuidString
// Pass IDFA to ad network SDK
configureAdSDKs(withIDFA: idfa)
case .denied, .restricted:
// Initialize SDK in tracking-free mode
configureAdSDKs(withIDFA: nil)
case .notDetermined:
break
@unknown default:
break
}
}
A common mistake — calling requestTrackingAuthorization before the application becomes .active. The dialog simply doesn't appear, the status remains .notDetermined, and the application never asks again. This is hard to reproduce in the simulator but easy to catch on a real device on first launch.
Integration with advertising SDKs
Facebook (Meta) Audience Network, Google AdMob, AppsFlyer, Adjust — all these SDKs must be initialized after obtaining the ATT status, otherwise they start without IDFA and cache this fact.
// AppDelegate or SceneDelegate
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, ...) {
Task {
await requestTrackingPermission()
// Only after this
initializeAnalyticsSDKs()
initializeAdSDKs()
}
}
SKAdNetwork: for attribution without IDFA. The SKAdNetworkItems list in Info.plist needs to be updated each time a new ad network is added. Meta, Google, Unity and other networks publish their SKAdNetwork identifiers. A tool for generating the current list — SKAdNetwork IDs from MMP providers.
AppsFlyer requires separate configuration for tracking-free mode:
AppsFlyerLib.shared().start()
// When denied/restricted
AppsFlyerLib.shared().anonymizeUser = true
Testing
The simulator doesn't show the ATT dialog. Test only on a real device. Reset the permission status for a specific application: Settings → Privacy → Tracking or complete app reinstallation.
For automated testing — ATTrackingManager can be mocked through a protocol in tests without accessing the real API.
Process
Audit of current ATT implementation and SDK initialization, verification of Info.plist.
Implementation of correct flow: display timing, handling all statuses, passing IDFA to SDKs.
Integration with specific advertising SDKs (Meta, AdMob, AppsFlyer/Adjust).
Testing on device, checking behavior with denied status.
Timeline estimates
ATT implementation with one or two SDKs — 1 day. With a complex matrix of advertising networks and SKAdNetwork configuration required — up to 3 days.







