ATT 追踪弹窗不出现 —— 完整修复指南

ATTrackingManager.requestTrackingAuthorization 不弹 UI 直接返回 notDetermined 或 denied,原因多在时机、设置或 Info.plist 缺失。逐项排查。

你在 App 首次启动时调 ATTrackingManager.requestTrackingAuthorization,但系统弹窗根本没出现 —— completion handler 立即拿到 .denied.notDetermined,IDFA 仍是全 0,归因 SDK(AppsFlyer、Adjust、Branch、Facebook)一直把安装报成 unattributed。有时本地真机能弹,TestFlight 或正式版上又消失。ATT 是那种”任何可见症状都能追溯到 6 个环境或配置问题之一”的 API;API 本身诚实可预测,只要你知道往哪看。

常见原因

按工单出现频率排序。

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 请求跟踪着的,没有任何 App 能弹窗 —— requestTrackingAuthorization 直接静默返回 .denied。iOS 14.5+ 用户里 70% 以上的人关掉了这个总开关。

如何识别:在出问题的设备上按上述路径检查,主开关关着的话每个 App 的 requestTrackingAuthorization 都会静默返回 .denied

3. App 还没进入前台 / active 状态就调用了

requestTrackingAuthorization 要求 applicationState == .active。在 application(_:didFinishLaunchingWithOptions:) 里调太早 —— applicationDidBecomeActive 触发前 App 都是 .inactive

如何识别:你在 AppDelegate 的 didFinishLaunchingWithOptions 里调 ATT。handler 拿到 .notDetermined,UI 没出现。

4. Apple ID 是未成年账号

苹果规定未成年用户(地区不同年龄阈值不同)不会被显示 ATT 弹窗。Apple ID 是儿童账号时,请求直接静默拒绝。

如何识别:测试设备的 Apple ID 是儿童 / 家庭账号。换成成人账号弹窗就出现。

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 前完成就能修复。

开始之前

  • 在真机上测 —— 模拟器返回 mock 值,对 ATT 不是可靠信号。
  • 记录 iOS 版本(ATT 要求 iOS 14.5+)。
  • 先检查设备级跟踪总开关。80% 的”弹窗不出现”工单在这里就能解决。

需要收集的信息

  • 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:紧贴启动后调的话加一小段延迟

iOS 偶尔会在 active 后头约 400ms 内静默吞掉弹窗。加一小段延迟:

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 提升同意率

ATT 平均同意率 20-25%。设计良好的 pre-prompt 页面(系统弹窗前由你自己 UI 解释价值交换)能把这数字提到 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
}

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 状态的办法)。

验证

  • iOS 14.5+ 真机、主开关开、未曾回答过的状态下,启动 App 看到系统 ATT 弹窗。
  • 同意后状态变为 .authorized,能拿到非 0 IDFA。
  • 拒绝后状态变为 .denied,IDFA 为全 0(00000000-0000-0000-0000-000000000000)。
  • 再次启动不重复弹,状态返回上次保存的值。
  • 归因 SDK 后台能看到 authorized 安装上报真实 IDFA。

长期预防

  • CI 中对 NSUserTrackingUsageDescription 缺失硬失败;弹窗对它的缺失是静默的,所以要显式检查。
  • 把设备级总开关写进测试者说明;每个 TestFlight 邀请都要提一句。
  • 做一个 QA 用的内部诊断页,打印 trackingAuthorizationStatusadvertisingIdentifier —— 能省下大量”ATT 到底有没有工作?“的调试时间。
  • 对 pre-prompt 文案做 A/B 测试;措辞的小变化能让同意率多 10+ 个百分点。
  • 把 SDK init 放在明确命名的”after-ATT”代码路径里;永远不要在 didFinishLaunchingWithOptions 里直接调用需要 IDFA 的 SDK。
  • iOS 大版本升级时复测一遍;苹果在 iOS 15、16、17 都以不显眼的方式收紧过 ATT 执行。

常见坑

  • 在自己的 pre-prompt 之前先弹系统弹窗。系统弹窗只有一次机会,pre-prompt 必须先。
  • 在一个 modal 里调 ATT —— 另一个 presentation 正在飞行时 iOS 有时会忽略请求。从根 scene 状态调。
  • 把弹窗描述本地化时把”跟踪”这个词改成”个性化”之类委婉说法 —— App Review 会拒,“tracking”是苹果的官方用词。
  • .denied 之后通过反复调 requestTrackingAuthorization 想重问。一旦拒掉只能让用户去设置里改,或重装 App。
  • .notDetermined 当成”用户拒绝了”—— 这只意味着弹窗根本没出现过。要查根因,不要默认当 .denied
  • 以为模拟器的 ATT 行为和真机一致。必须真机复测。隐私层面的回应策略还可以参考App 因 5.1.1 隐私条款被拒

FAQ

Q:不实现 ATT 会被苹果拒吗?

只有当你用 IDFA 或跨 App / 站点跟踪而又没弹这个弹窗才会被拒。如果你完全不跟踪、只用第一方数据,你完全不需要调 ATT —— 而且不需要时不应该调,那属于误导用户。

Q:用户同意了,IDFA 还是全 0,为什么?

两个常见原因:(1) 你在 ATT 完成前就收了 IDFA,SDK 缓存了 0;(2) 设备级”允许 App 请求跟踪”是关着的 —— 这种情况下同意也返回 authorized,但 IDFA 仍是 0。苹果用这种方式强制设备级总开关。

Q:用 SKAdNetwork 能绕开 ATT 吗?

SKAdNetwork 是苹果提供的保护隐私的归因通道,不需要 ATT,给出的是聚合、有延迟的安装归因。如果是基于 IDFA 的个人级归因,ATT 仍是必需。

Q:测试者同意之后删了 App 重装,弹窗会再出现吗?

会。删 App 会重置该 App 在该设备上的 ATT 状态。在设置里重置 Apple ID 的广告标识符也一样。

Q:TestFlight 算不算 ATT 的”首次启动”?

算。TestFlight 上的首次安装会弹窗。同一个 CFBundleIdentifier 的后续构建除非用户卸载否则不再弹。

相关阅读

标签: #排查 #ios #att #idfa #privacy