You call ATTrackingManager.requestTrackingAuthorization on first launch, but no system dialog appears — the completion handler fires immediately with .denied or .notDetermined, your IDFA stays zeroed out, and attribution SDKs (AppsFlyer, Adjust, Branch, Facebook) keep reporting unattributed installs. Sometimes the prompt shows in development on your personal device but vanishes once the app ships to TestFlight or production. ATT is one of those APIs where every visible symptom (no prompt, denied without asking, zero IDFA) traces back to one of six environmental or configuration issues; the API itself is honest and predictable once you know where to look.
Common causes
Ordered by how often each shows up in support tickets.
1. NSUserTrackingUsageDescription missing or empty
Without the Info.plist key, iOS does not display the prompt and the completion handler returns .denied. Empty strings get the same treatment.
How to spot it: grep NSUserTrackingUsageDescription Info.plist returns nothing, or the value is "". Console: “Cannot request tracking authorization without a value for NSUserTrackingUsageDescription.”
2. User has disabled “Allow Apps to Request to Track” in Settings
If Settings → Privacy & Security → Tracking → Allow Apps to Request to Track is off, no app can prompt — requestTrackingAuthorization returns .denied without showing UI. This is the device-wide kill switch that 70%+ of iOS 14.5+ users have flipped.
How to spot it: Go to that Settings path on the affected device. If the master toggle is off, every app’s requestTrackingAuthorization will silently return .denied.
3. Called before the app is in the foreground / active
requestTrackingAuthorization requires applicationState == .active. Calls in application(_:didFinishLaunchingWithOptions:) happen too early — the app is .inactive until applicationDidBecomeActive fires.
How to spot it: You call ATT in AppDelegate’s didFinishLaunchingWithOptions. The handler fires with .notDetermined and no UI appears.
4. Age gated below the threshold
Per Apple, users under 18 (varies by region) cannot be shown the ATT prompt. If the Apple ID’s child account flag is set, the request is auto-denied with no UI.
How to spot it: Test device’s Apple ID is a child / family account. Switch to an adult account and the prompt appears.
5. Restricted by MDM or Screen Time
Enterprise MDM profiles or Screen Time content restrictions can disable tracking system-wide. In Screen Time → Content & Privacy Restrictions → Allow Changes → Tracking, “Don’t Allow” silently denies all ATT requests.
How to spot it: Device is enrolled in MDM, or Screen Time restrictions are active. Settings → Screen Time → Content & Privacy Restrictions shows Tracking locked.
6. Prompt was already answered for this app
ATT only prompts once per install. If you (or a previous tester) answered the prompt and then signed in again on the same device, the saved answer sticks. trackingAuthorizationStatus returns the prior decision; the completion handler fires without UI.
How to spot it: ATTrackingManager.trackingAuthorizationStatus != .notDetermined before you even call requestTrackingAuthorization. The answer is whatever was given previously.
7. SDK init order: ATT requested AFTER an SDK that already captured IDFA
Some attribution SDKs read IDFA during init. If you call requestTrackingAuthorization after SDK init, the SDK already cached a zeroed IDFA. The prompt may still appear, but downstream attribution is broken.
How to spot it: SDK reports zero IDFA even after user accepts. Re-installing the app and ensuring ATT runs before SDK init fixes it.
Before you start
- Test on a real device — the simulator returns canned values and is not a reliable signal for ATT.
- Note the iOS version (ATT requires iOS 14.5+).
- Check the device-wide tracking toggle FIRST. 80% of “prompt not appearing” tickets resolve here.
Information to collect
- Output of
grep -A1 NSUserTrackingUsageDescription Info.plist(or the equivalent build setting value). - Current value of
ATTrackingManager.trackingAuthorizationStatusbefore you callrequestTrackingAuthorization. - The exact callsite of
requestTrackingAuthorization(file + line) and which lifecycle method it runs in. - iOS version of the test device.
- Settings → Privacy & Security → Tracking → Allow Apps to Request to Track toggle state.
- Whether the device is an enterprise MDM device or a child / family account.
Step-by-step fix
Step 1: Verify Info.plist key with a real description
<key>NSUserTrackingUsageDescription</key>
<string>Your data helps us show you relevant ads and measure ad performance.</string>
App Review enforces specificity. “We need tracking” is rejected; describe the user-visible benefit. Localize via InfoPlist.strings for every supported language.
Step 2: Confirm device-wide tracking is allowed
On the test device:
Settings → Privacy & Security → Tracking → Allow Apps to Request to Track = ON
If off, no app’s prompt will appear. Educate testers in your TestFlight notes that this toggle must be on for ATT QA.
Step 3: Call ATT only when app is .active
Move the call out of didFinishLaunchingWithOptions into a method that runs after foregrounding:
import AppTrackingTransparency
import AdSupport
func requestTrackingPermission() {
if #available(iOS 14.5, *) {
ATTrackingManager.requestTrackingAuthorization { status in
switch status {
case .authorized:
let idfa = ASIdentifierManager.shared().advertisingIdentifier
AnalyticsSDK.shared.setIDFA(idfa.uuidString)
case .denied, .restricted, .notDetermined:
AnalyticsSDK.shared.setIDFA(nil)
@unknown default:
AnalyticsSDK.shared.setIDFA(nil)
}
}
}
}
Invoke from sceneDidBecomeActive (SwiftUI: from a .onAppear of your root view after a small delay) or from a deliberate first-launch onboarding screen.
Step 4: Add a small delay if you call right after launch
iOS occasionally drops the prompt if called within the first ~400ms of becoming active. Add a small delay:
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
requestTrackingPermission()
}
In a SwiftUI .task:
.task {
try? await Task.sleep(nanoseconds: 500_000_000)
requestTrackingPermission()
}
Step 5: Re-order SDK init relative to ATT
The recommended pattern: collect IDFA after ATT completion, then pass to SDKs:
ATTrackingManager.requestTrackingAuthorization { status in
let idfa: String? = (status == .authorized)
? ASIdentifierManager.shared().advertisingIdentifier.uuidString
: nil
AppsFlyerLib.shared().advertisingIdentifier = idfa
AppsFlyerLib.shared().start()
Adjust.appDidLaunch(adjustConfig)
}
If the SDK absolutely requires init at app launch (e.g. for deep link handling), use the SDK’s delayInitForATT / waitForATTUserAuthorization configuration. Both AppsFlyer and Adjust expose this.
Step 6: Show a pre-prompt to lift acceptance rate
ATT acceptance averages 20-25%. A well-designed pre-prompt screen (your own UI explaining the value exchange before the system alert) lifts that to 40-60%:
NavigationView {
VStack(spacing: 16) {
Image(systemName: "person.crop.circle.badge.checkmark")
.font(.system(size: 64))
Text("Help us improve your experience")
.font(.title)
Text("Allow tracking to see personalized content and support free features.")
Button("Continue") {
showSystemPrompt = true
}
}
}
.alert(isPresented: $showSystemPrompt) {
// request actual ATT here on dismiss
}
The pre-prompt is your sales pitch; the system alert is the decision moment.
Step 7: Handle the “already-answered” case
Before calling requestTrackingAuthorization, check status:
let status = ATTrackingManager.trackingAuthorizationStatus
switch status {
case .notDetermined:
requestTrackingPermission()
case .denied, .restricted:
// Send user to Settings if you want to give a second chance
if let url = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url)
}
case .authorized:
break // already approved, nothing to do
@unknown default: break
}
For testers who already declined and want to retest, have them delete the app and reinstall (the only way to reset ATT state for that app).
Verify
- On a real iOS 14.5+ device with master toggle ON and tracking authorization not yet answered, launching the app shows the system ATT alert.
- Accepting the prompt returns
.authorizedand a non-zero IDFA. - Declining returns
.deniedand IDFA is zero (00000000-0000-0000-0000-000000000000). - Re-launching the app does not re-prompt; status returns the saved value.
- Attribution SDK dashboard shows authorized installs reporting a real IDFA.
Long-term prevention
- Hard-fail on missing
NSUserTrackingUsageDescriptionin CI; the prompt is silent about its absence, so add an explicit check. - Document the device-wide toggle in your tester onboarding notes; every TestFlight invite should mention it.
- Build a small in-app diagnostic screen for QA that prints
trackingAuthorizationStatusandadvertisingIdentifier— saves hours of “is ATT working?” debug time. - Run an A/B test of pre-prompt copy; small wording changes move acceptance by 10+ percentage points.
- Keep SDK init in a clearly named “after-ATT” code path; never call SDKs that need IDFA from
didFinishLaunchingWithOptionsdirectly. - Re-check on each iOS major upgrade; Apple has tightened ATT enforcement in iOS 15, 16, 17 in non-obvious ways.
Common pitfalls
- Showing the system prompt before your pre-prompt. Once the system prompt is dismissed, you only get one shot; the pre-prompt must come first.
- Calling ATT inside a presented modal — iOS sometimes ignores the request when another presentation is in flight. Call from the root scene state.
- Localizing the prompt description but forgetting that “tracking” itself is Apple’s word; do not euphemize it as “personalization” — App Review rejects.
- Re-asking after
.deniedvia repeatedrequestTrackingAuthorizationcalls. Once denied, only re-installing or user navigating to Settings can change the answer. - Treating
.notDeterminedas “user said no” — it means the prompt was never shown. Investigate the failure rather than defaulting to.denied. - Assuming the simulator’s ATT prompt behavior matches a real device. Always retest on hardware. See App Rejected for Guideline 5.1.1 Privacy for the privacy framing reviewer also checks.
FAQ
Q: Will Apple reject me for not implementing ATT?
Only if you use IDFA or track across apps/sites without the prompt. If your app is tracking-free and uses only first-party data, you do not need to call ATT at all — and you should not, since calling it without need is misleading.
Q: My IDFA is zero even after the user accepts. Why?
Two common reasons: (1) you collected the IDFA before ATT completed, so the SDK cached zero; (2) the device-wide Allow Apps to Request to Track is off — in that case acceptance returns authorized but IDFA still returns zero. Apple does this to enforce the device-wide toggle.
Q: Can I bypass ATT with SKAdNetwork?
SKAdNetwork is the privacy-preserving attribution path Apple offers; it does not need ATT and provides aggregate, delayed install attribution. For per-user IDFA-based attribution, ATT remains required.
Q: Tester accepted, then deleted and reinstalled. Will the prompt show again?
Yes. Deleting the app resets ATT state for that app on that device. Same for resetting the Apple ID’s advertising identifier in Settings.
Q: Does TestFlight count as “first run” for ATT?
Yes, the first TestFlight install presents the prompt. Subsequent rebuilds of the same CFBundleIdentifier do not re-prompt unless the user uninstalls.
Related
- App Rejected for Guideline 5.1.1 Privacy
- App Crashes on Launch From Missing Usage Description
- App Privacy Questionnaire Rejected
- App Store Localization Confusion
Tags: #Troubleshooting #ios #att #idfa #privacy