你在 App 首次启动时调 ATTrackingManager.requestTrackingAuthorization,但系统弹窗根本没出现。completion handler 立即拿到 .denied 或 .notDetermined,IDFA 仍是全 0,归因 SDK(AppsFlyer、Adjust、Branch、Meta)一直把安装报成 unattributed。有时本地真机能弹,TestFlight 或正式版上又消失。
先试最快的修复: 最常见的原因是在 App 进入前台之前就调了这个 API。iOS 只在 UIApplication.shared.applicationState == .active 时才弹窗——这条规则从 iOS 15 起就强制执行,iOS 18 依然如此。如果你在 didFinishLaunchingWithOptions 里调 requestTrackingAuthorization,此时 App 还是 .inactive,iOS 会静默丢弃请求并返回 .notDetermined。把调用挪到 sceneDidBecomeActive(SwiftUI 里放到首屏的 .task)并加一小段延迟,再检查设备级跟踪总开关。这两步就能解决绝大多数工单。
任何可见症状——不弹窗、没问就被拒、IDFA 全 0——都能追溯到下面某一类。API 本身诚实可预测,只要你知道往哪看。
你属于哪一类?
| 你看到的现象 | 最可能的原因 | 跳转 |
|---|---|---|
返回 .notDetermined,无 UI | 在 .active 之前调用,或延迟 / 并发弹窗问题 | 原因 3、8 |
立即返回 .denied,无 UI | 设备总开关关、Info.plist key 缺失、或已回答过 | 原因 1、2、6 |
返回 .restricted | 儿童账号、MDM 或屏幕使用时间 | 原因 4、5 |
| 弹窗出现了但 IDFA 仍是 0 | 设备总开关关,或 SDK 在 ATT 完成前读了 IDFA | 原因 2、7 |
| 真机能弹,扩展 / 小组件里从不弹 | 从 App 扩展里调用 | 原因 9 |
常见原因
按工单出现频率排序。
1. NSUserTrackingUsageDescription 缺失或为空
没这个 Info.plist key 时 iOS 不弹窗,handler 直接返回 .denied。空字符串同等待遇。
如何识别:grep NSUserTrackingUsageDescription Info.plist 没输出,或值为 ""。Console:“Cannot request tracking authorization without a value for NSUserTrackingUsageDescription.”
2. 设备级跟踪总开关关着,或根本打不开
如果 设置 → 隐私与安全性 → 跟踪 → 允许 App 请求跟踪是关着的,没有任何 App 能弹窗,requestTrackingAuthorization 直接返回 .denied,不显示 UI。这个总开关对符合条件的账号默认是开的,但不少用户会主动关掉,而且在某些账号上它根本打不开。
根据苹果当前的支持文档(截至 2026 年 6 月),出现以下任一情况时该开关不可用、弹窗也永远不会出现:
- Apple Account 是儿童账号,或按出生年份属于未满 18 岁的用户(年龄阈值因国家 / 地区而异)。
- Apple Account 由教育或企业机构管理(受管理的 Apple 账户)。
- 设备处于受管理状态,且使用了限制跟踪的配置描述文件。
- Apple Account 是最近 3 天内创建的。
如何识别:在出问题的设备上按上述路径检查。主开关关着或变灰时,每个 App 的 requestTrackingAuthorization 都会返回 .denied(受管理 / 儿童账号则返回 .restricted),且无 UI。权威清单见苹果的 If an app asks to track your activity。
3. App 还没进入前台 / active 状态就调用了
这是头号原因。从 iOS 15 起(iOS 18 仍然如此),requestTrackingAuthorization 只在 applicationState == .active 时才弹窗。在 application(_:didFinishLaunchingWithOptions:) 里调太早了,applicationDidBecomeActive(或 sceneDidBecomeActive)触发前 App 都是 .inactive,iOS 会丢掉这个请求。
如何识别:你在 AppDelegate 的 didFinishLaunchingWithOptions 里调 ATT。handler 拿到 .notDetermined,UI 没出现。在调用处打断点并打印 UIApplication.shared.applicationState——如果不是 .active,这就是你的 bug。
4. 儿童账号或未满 18 岁的 Apple Account
苹果规定未满 18 岁(阈值因地区而异)的用户和儿童账号不会被显示 ATT 弹窗。该开关变灰,requestTrackingAuthorization 返回 .restricted(不是 .denied),无 UI。
如何识别:测试设备的 Apple Account 是儿童 / 家庭受管理账号。trackingAuthorizationStatus 返回 .restricted。换成成人、未受管理的账号弹窗就出现。
5. MDM 或屏幕使用时间禁用了跟踪
企业 MDM profile 或屏幕使用时间内容限制可以全局禁用跟踪。屏幕使用时间 → 内容和隐私访问限制 → 允许更改 → 跟踪,“不允许”会静默拒绝所有 ATT 请求。
如何识别:设备在 MDM 管控中,或屏幕使用时间限制启用了。设置 → 屏幕使用时间 → 内容和隐私访问限制显示跟踪被锁。
6. 本次安装里已经回答过弹窗
每次安装 ATT 只弹一次。如果你(或别的测试者)已经回答过,之后再调,保存的答案就一直生效。trackingAuthorizationStatus 返回上一次的决定,handler 不带 UI 就触发。
如何识别:在 requestTrackingAuthorization 之前 ATTrackingManager.trackingAuthorizationStatus != .notDetermined。答案就是上次给的。
7. SDK 在 ATT 之前已经初始化并读了 IDFA
某些归因 SDK 在 init 时就读 IDFA。如果你在 SDK init 之后才调 requestTrackingAuthorization,SDK 已经缓存了全 0 的 IDFA。弹窗可能照常出现,但下游归因已经废了。
如何识别:用户同意后 SDK 报告的 IDFA 仍为 0。重装 App 并确保 ATT 在 SDK init 前完成就能修复。
8. 屏幕上已经有另一个权限弹窗
iOS 一次只显示一个系统权限弹窗,且不会把 ATT 排队到别的弹窗后面。如果你在通知、定位、相机或 App Store 登录弹窗还在(或正在消失)的时候调 requestTrackingAuthorization,iOS 会丢掉这次 ATT 请求,handler 返回 .notDetermined,无 UI。这就是为什么 ATT 单独测时”正常”,可一旦放进堆叠多个弹窗的真实 onboarding 流程里就消失。
如何识别:单独测 ATT 时弹窗出现,但紧跟在另一个权限请求之后就不出现。逐个串行申请权限,等上一个的 completion handler 回来再调下一个。
9. 从 App 扩展或小组件里调用
requestTrackingAuthorization 只在主 App 进程里弹窗。从 App 扩展、小组件(widget)或 App Clip 里调用不会显示 UI,会直接返回、不弹窗。
如何识别:调用写在了非主 App 的 target 里(Today 小组件、share 扩展、通知 service 等)。把请求挪进主 App,并把需要 IDFA 的功能放在它之后。
开始之前
- 在真机上测。模拟器返回 mock 值,对 ATT 不是可靠信号。
- 记录 iOS 版本(ATT 要求 iOS 14.5+;
.active状态规则从 iOS 15 起适用)。 - 先检查调用位置(是不是
.active?)和设备级跟踪总开关。大多数”弹窗不出现”的工单都在这两处之一解决。
需要收集的信息
grep -A1 NSUserTrackingUsageDescription Info.plist的输出(或对应 build setting 值)。- 调用
requestTrackingAuthorization之前ATTrackingManager.trackingAuthorizationStatus的当前值。 requestTrackingAuthorization的具体调用位置(文件 + 行),以及它跑在哪个生命周期方法里。- 测试设备的 iOS 版本。
- 设置 → 隐私与安全性 → 跟踪 → 允许 App 请求跟踪的状态。
- 设备是否在企业 MDM 中,或者是不是儿童 / 家庭账号。
修复步骤
步骤 1:确认 Info.plist key 写了一段真正的描述
<key>NSUserTrackingUsageDescription</key>
<string>Your data helps us show you relevant ads and measure ad performance.</string>
App Review 强制要求具体。“We need tracking”会被拒;要说明用户能看到的好处。每种支持的语言都通过 InfoPlist.strings 本地化。
步骤 2:确认设备级跟踪允许
测试设备:
设置 → 隐私与安全性 → 跟踪 → 允许 App 请求跟踪 = 开
关着时所有 App 的弹窗都不会出现。在你的 TestFlight 测试说明里告诉测试者:这个开关必须开才能测 ATT。
步骤 3:只在 App .active 时调 ATT
把调用从 didFinishLaunchingWithOptions 挪到进入前台之后才跑的方法:
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)
}
}
}
}
从 sceneDidBecomeActive(SwiftUI:在根视图 .onAppear 里加一小段延迟)或者一个明确的首次启动 onboarding 页面里触发。
步骤 4:紧贴启动后调的话加一小段延迟
如果在 App 刚进入 active、scene 还没完全稳定的最初一瞬间就请求,iOS 偶尔会静默吞掉弹窗。加一小段延迟:
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
requestTrackingPermission()
}
SwiftUI .task 里:
.task {
try? await Task.sleep(nanoseconds: 500_000_000)
requestTrackingPermission()
}
步骤 5:调整 SDK init 相对于 ATT 的顺序
推荐模式:ATT 完成后再收 IDFA,再传给 SDK:
ATTrackingManager.requestTrackingAuthorization { status in
let idfa: String? = (status == .authorized)
? ASIdentifierManager.shared().advertisingIdentifier.uuidString
: nil
AppsFlyerLib.shared().advertisingIdentifier = idfa
AppsFlyerLib.shared().start()
Adjust.appDidLaunch(adjustConfig)
}
如果 SDK 必须在 App 启动时 init(比如要处理 deeplink),用 SDK 自己的 delayInitForATT / waitForATTUserAuthorization 配置。AppsFlyer 和 Adjust 都提供这种选项。
步骤 6:用 pre-prompt 提升同意率
截至 2026 年 6 月,按 Adjust 的 2025 年基准,ATT 同意率(被显示弹窗的用户中允许跟踪的比例)行业整体在 30-35% 左右,AppsFlyer 的样本里部分地区和品类要明显更高。设计良好的 pre-prompt 页面——系统弹窗前由你自己 UI 解释价值交换——能可靠地多拿几个百分点,因为它只把系统弹窗显示给大概率会同意的用户:
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
}
Pre-prompt 是你的销售环节,系统弹窗才是用户做决定的瞬间。
步骤 7:处理”已经回答过”的情况
调用 requestTrackingAuthorization 之前先检查状态:
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
}
已经拒过又想重测的测试者,需要删 App 重装(唯一能重置那个 App ATT 状态的办法)。
步骤 8:权限一次只申请一个
iOS 不会给系统权限弹窗排队,所以绝不要把 ATT 和别的弹窗同时弹。用 completion handler 把它们串起来,让 ATT 要么排在最前,要么等所有其他弹窗都处理完之后再弹:
UNUserNotificationCenter.current()
.requestAuthorization(options: [.alert, .badge, .sound]) { _, _ in
DispatchQueue.main.async {
// 通知已处理完;现在在主线程申请 ATT。
requestTrackingPermission()
}
}
如果你的 onboarding 还有定位、相机或通知弹窗,等每一个的 completion handler 回来再弹下一个。两个弹窗在同一个 run loop 里触发,其中一个(往往就是 ATT)会被静默丢弃。
验证
- iOS 14.5+ 真机、设备级总开关开、未曾回答过的状态下,App 在
.active时启动会看到系统 ATT 弹窗。 - 调用前
ATTrackingManager.trackingAuthorizationStatus读到.notDetermined,调用后读到对应的值。 - 同意后状态变为
.authorized,能拿到非 0 IDFA。 - 拒绝后状态变为
.denied,IDFA 为全 0(00000000-0000-0000-0000-000000000000)。 - 再次启动不重复弹,状态返回上次保存的值。
- 归因 SDK 后台能看到 authorized 安装上报真实 IDFA。
长期预防
- CI 中对
NSUserTrackingUsageDescription缺失硬失败;弹窗对它的缺失是静默的,所以要显式检查。 - 把设备级总开关写进测试者说明;每个 TestFlight 邀请都要提一句。
- 做一个 QA 用的内部诊断页,打印
trackingAuthorizationStatus和advertisingIdentifier,能省下大量”ATT 到底有没有工作?“的调试时间。 - 对 pre-prompt 文案做 A/B 测试;措辞的变化能让同意率波动几个百分点。
- 把 SDK init 放在明确命名的”after-ATT”代码路径里;永远不要在
didFinishLaunchingWithOptions里直接调用需要 IDFA 的 SDK。 - iOS 大版本升级时复测一遍;
.active状态要求是 iOS 15 引入的、一直延续到 iOS 18,但苹果在各版本里都以不显眼的方式收紧过 ATT 执行。
常见坑
- 在自己的 pre-prompt 之前先弹系统弹窗。系统弹窗只有一次机会,pre-prompt 必须先。
- 在一个 modal 里调 ATT,另一个 presentation 正在飞行时 iOS 有时会忽略请求。从根 scene 状态调。
- 把弹窗描述本地化时把”跟踪”这个词改成”个性化”之类委婉说法,App Review 会拒,“tracking”是苹果的官方用词。
.denied之后通过反复调requestTrackingAuthorization想重问。一旦拒掉只能让用户去设置里改,或重装 App。- 把
.notDetermined当成”用户拒绝了”,这只意味着弹窗根本没出现过。要查根因,不要默认当.denied。 - 以为模拟器的 ATT 行为和真机一致。必须真机复测。隐私层面 reviewer 还会检查的回应策略,可参考App 因 5.1.1 隐私条款被拒。
FAQ
Q:不实现 ATT 会被苹果拒吗?
只有当你用 IDFA 或跨 App / 站点跟踪而又没弹这个弹窗才会被拒。如果你完全不跟踪、只用第一方数据,你完全不需要调 ATT,而且不需要时不应该调,那属于误导用户。
Q:用户同意了,IDFA 还是全 0,为什么?
两个常见原因:(1) 你在 ATT 完成前就收了 IDFA,SDK 缓存了 0;(2) 设备级”允许 App 请求跟踪”是关着的,这种情况下同意也返回 authorized,但 IDFA 仍是 0。苹果用这种方式强制设备级总开关。
Q:.notDetermined、.denied、.restricted 有什么区别?
.notDetermined 表示弹窗从未显示过,所以这是你调 API 之前应该看到的值;如果调用之后还看到它,说明弹窗被静默丢弃了(状态不是 .active、并发弹窗、扩展等)。.denied 表示用户或设备总开关说了不。.restricted 表示系统因账号 / 设备原因(儿童账号、MDM、屏幕使用时间)拦下了弹窗,用户也改不了。把 .restricted 当成”无法询问”,而不是”用户拒绝了”。
Q:用 SKAdNetwork 或 AdAttributionKit 能绕开 ATT 吗?
两者都是苹果提供的保护隐私的归因通道,都不需要 ATT,给出的是聚合、有延迟的安装归因。AdAttributionKit 是较新的框架,对新安装取代了 SKAdNetwork,而 SKAdNetwork 仍受支持。如果是基于 IDFA 的个人级归因,ATT 仍是必需。
Q:测试者同意之后删了 App 重装,弹窗会再出现吗?
会。删 App 会重置该 App 在该设备上的 ATT 状态。在设置里重置 Apple ID 的广告标识符也一样。
Q:TestFlight 算不算 ATT 的”首次启动”?
算。TestFlight 上的首次安装会弹窗。同一个 CFBundleIdentifier 的后续构建除非用户卸载否则不再弹。