关于 axiom-privacy-ux
面向 Apple 平台开发者的隐私体验设计技能,说明隐私清单、系统权限、ATT、追踪域名和必需理由 API 的处理方式。还涵盖 App Store 隐私标签及隐私相关审核问题。
主要功能
- 01编写 PrivacyInfo.xcprivacy 清单
- 02设计适时的系统权限请求
- 03实施 App Tracking Transparency
- 04管理追踪域名与必需理由 API
- 05准备 App Store 隐私标签
适用场景
- 01为应用声明收集的数据和 API 用途
- 02在功能触发时请求相机或定位权限
- 03检查追踪连接及 ATT 行为
- 04排查隐私相关的 App Store 审核拒绝
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ charleswiltgen/ axiom --skill axiom-privacy-ux安装后在 Claude Code 中输入/axiom-privacy-ux确认已加载。
详细介绍
隐私体验设计模式
隐私优先应用设计综合指南。Apple Design Award 社会影响力奖项的获奖者以合乎伦理的方式处理数据,隐私优先设计是关键差异化优势。
概述
隐私清单(PrivacyInfo.xcprivacy)是 Apple 用于提高数据收集和跟踪透明度的框架。结合 App Tracking Transparency 和适时权限请求,它们有助于用户就自己的数据做出明智选择。
本技能涵盖创建隐私清单、以出色的用户体验请求系统权限、实施 App Tracking Transparency、管理跟踪域名、使用 Required Reason API,以及准备准确的隐私营养标签。
何时使用本技能
- 创建隐私清单(PrivacyInfo.xcprivacy)
- 请求系统权限(相机、位置等)
- 实施 App Tracking Transparency(ATT)
- 为 App Store Connect 准备隐私营养标签
- 管理跟踪域名,避免意外跟踪
- 使用 Required Reason API(NSFileSystemFreeSize、UserDefaults 等)
- 向用户透明地说明数据用途
- 调试与隐私相关的 App Store 审核拒绝
系统要求
- iOS 14.5+ 才支持 App Tracking Transparency
- iOS 17+ 才支持自动屏蔽跟踪域名
- Xcode 15+ 才支持隐私报告和清单编辑
- 2024 年春季+ 开始由 App Review 强制执行
第 1 部分:隐私清单(WWDC 2023/10060)
创建隐私清单
Xcode 导航器:
- File → New → File
- 选择“App Privacy File”
- 名称:
PrivacyInfo.xcprivacy - 添加到应用目标(或 SDK 框架)
文件结构(属性列表):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyCollectedDataTypes</key>
<array>
</array>
<key>NSPrivacyAccessedAPITypes</key>
<array>
</array>
</dict>
</plist>
NSPrivacyTracking 声明
你的应用会跟踪用户吗?
跟踪 = 将来自你的应用的用户/设备数据与来自其他应用/网站的数据结合起来,用于创建用户画像,以便进行定向广告或供数据经纪商使用。
<key>NSPrivacyTracking</key>
<true/>
如果为 true,还必须声明跟踪域名:
<key>NSPrivacyTrackingDomains</key>
<array>
<string>tracking.example.com</string>
<string>analytics.example.com</string>
</array>
iOS 17 行为:如果用户未授予 ATT 权限,则会自动屏蔽发往跟踪域名的网络请求。
NSPrivacyCollectedDataTypes
声明应用收集的所有数据:
<key>NSPrivacyCollectedDataTypes</key>
<array>
<dict>
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypeName</string>
<key>NSPrivacyCollectedDataTypeLinked</key>
<true/>
<key>NSPrivacyCollectedDataTypeTracking</key>
<false/>
<key>NSPrivacyCollectedDataTypePurposes</key>
<array>
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
<string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
</array>
</dict>
</array>
常见数据类型:
NSPrivacyCollectedDataTypeName- 用户姓名NSPrivacyCollectedDataTypeEmailAddressNSPrivacyCollectedDataTypePhoneNumberNSPrivacyCollectedDataTypePhysicalAddressNSPrivacyCollectedDataTypePreciseLocationNSPrivacyCollectedDataTypeCoarseLocationNSPrivacyCollectedDataTypePhotosorVideosNSPrivacyCollectedDataTypeContactsNSPrivacyCollectedDataTypeUserID
常见用途:
NSPrivacyCollectedDataTypePurposeAppFunctionalityNSPrivacyCollectedDataTypePurposeAnalyticsNSPrivacyCollectedDataTypePurposeProductPersonalizationNSPrivacyCollectedDataTypePurposeDeveloperAdvertisingNSPrivacyCollectedDataTypePurposeThirdPartyAdvertising
NSPrivacyAccessedAPITypes
声明 Required Reason API(参见第 5 部分):
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>C617.1</string>
</array>
</dict>
</array>
第 2 部分:权限请求体验
适时请求与启动时请求
❌ 不要:在启动时请求所有权限
// BAD - overwhelming and confusing
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
requestCameraPermission()
requestLocationPermission()
requestNotificationPermission()
requestPhotoLibraryPermission()
return true
}
✅ 应当:在用户触发功能时适时请求
// GOOD - clear causality
@objc func takePhotoButtonTapped() {
// Show pre-permission education first
showCameraEducation {
// Then request permission
AVCaptureDevice.requestAccess(for: .video) { granted in
if granted {
self.openCamera()
} else {
self.showPermissionDeniedAlert()
}
}
}
}
权限请求前的说明界面
在显示系统对话框之前,说明你需要权限的原因:
func showCameraEducation(completion: @escaping () -> Void) {
let alert = UIAlertController(
title: "Take Photos",
message: "FoodSnap needs camera access to let you photograph your meals and get nutrition information.",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "Continue", style: .default) { _ in
completion() // Now request actual permission
})
alert.addAction(UIAlertAction(title: "Not Now", style: .cancel))
present(alert, animated: true)
}
这样做的好处:
- 用户理解其价值
- 系统对话框的拒绝率降低 60-80%
- App Store 评分更高(“为什么需要这个权限?”之类的评价更少)
处理权限被拒绝的情况
绝不要让用户陷入死胡同:
func handleCameraPermission() {
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized:
openCamera()
case .notDetermined:
showCameraEducation {
AVCaptureDevice.requestAccess(for: .video) { granted in
if granted {
self.openCamera()
} else {
self.showSettingsPrompt()
}
}
}
case .denied, .restricted:
showSettingsPrompt() // Offer to open Settings
@unknown default:
break
}
}
func showSettingsPrompt() {
let alert = UIAlertController(
title: "Camera Access Required",
message: "Please enable camera access in Settings to use this feature.",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "Open Settings", style: .default) { _ in
if let url = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url)
}
})
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
present(alert, animated: true)
}
设置深层链接
打开特定的设置页面:
// General app settings
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
// Notification settings (iOS 15.4+)
UIApplication.shared.open(URL(string: UIApplication.openNotificationSettingsURLString)!)
第 3 部分:App Tracking Transparency
需要 ATT 的情况
如果你:
- 跨其他公司拥有的 App/网站跟踪用户
- 与数据经纪商共享用户数据
- 使用会跟踪用户的第三方 SDK(Facebook SDK、Google Analytics 等)
必须请求 ATT 权限。
如果你仅:
- 使用第一方分析(不与其他公司共享数据)
- 仅根据你自己的 App 中的数据个性化广告
- 使用防欺诈/安全措施
则不需要 ATT。
ATTrackingManager.requestTrackingAuthorization
import AppTrackingTransparency
import AdSupport
func requestTrackingPermission() {
// Check availability (iOS 14.5+)
guard #available(iOS 14.5, *) else { return }
// Wait until app is active
// Showing alert too early causes auto-denial
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
ATTrackingManager.requestTrackingAuthorization { status in
switch status {
case .authorized:
// User granted permission
// You can now access IDFA and track
let idfa = ASIdentifierManager.shared().advertisingIdentifier
self.initializeTrackingSDKs(idfa: idfa)
case .denied:
// User denied permission
// Do NOT track
self.initializeNonTrackingSDKs()
case .notDetermined:
// User closed dialog without choosing
// Treat as denied
self.initializeNonTrackingSDKs()
case .restricted:
// Device doesn't allow tracking (parental controls)
self.initializeNonTrackingSDKs()
@unknown default:
self.initializeNonTrackingSDKs()
}
}
}
}
自定义 ATT 提示信息
Info.plist:
<key>NSUserTrackingUsageDescription</key>
<string>This allows us to show you personalized ads and improve your experience</string>
最佳实践:
- 诚实且具体
- 说明对用户的好处(而非对公司的好处)
- 简明扼要(1~2 句话)
❌ 不好的示例:
- “我们重视你的隐私”(含糊)
- “这是 App 正常运行所必需的”(不诚实)
- “为了让我们的 App 盈利”(用户并不关心)
✅ 好的示例:
- “这有助于我们向你展示你可能感兴趣的产品广告”
- “个性化广告有助于让这款 App 保持免费”
跟踪前提示设计
在 ATT 系统提示前,先显示你自己的对话框:
func showPreTrackingPrompt() {
let alert = UIAlertController(
title: "Support Free Features",
message: "We use tracking to show you personalized ads, which helps keep advanced features free. You can always change this in Settings.",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "Continue", style: .default) { _ in
self.requestTrackingPermission()
})
alert.addAction(UIAlertAction(title: "Not Now", style: .cancel))
present(alert, animated: true)
}
这样做的原因:说明情况可将用户选择同意的比例提高 20~40%。
优雅降级
即使不跟踪,也始终提供价值:
func initializeAnalytics() {
let status = ATTrackingManager.trackingAuthorizationStatus
if status == .authorized {
// Full featured analytics
Analytics.setUserProperty(userID, forName: "user_id")
Analytics.enableCrossAppTracking()
} else {
// Limited, privacy-preserving analytics
Analytics.setUserProperty("anonymous", forName: "user_id")
Analytics.disableCrossAppTracking()
Analytics.enableOnDeviceConversionTracking()
}
}
第 4 部分:跟踪域名管理
声明跟踪域名
在 PrivacyInfo.xcprivacy 中:
<key>NSPrivacyTracking</key>
<true/>
<key>NSPrivacyTrackingDomains</key>
<array>
<string>tracking.example.com</string>
<string>ads.example.com</string>
</array>
iOS 17 的行为:如果用户拒绝 ATT,发往这些域名的网络请求会被自动拦截。
域名隔离策略
问题:跟踪和非跟踪功能使用同一个域名
解决方案:将功能拆分到不同的主机
Before:
- api.example.com (mixed tracking + app functionality)
After:
- api.example.com (app functionality only)
- tracking.example.com (tracking only)
更新清单:
<key>NSPrivacyTrackingDomains</key>
<array>
<string>tracking.example.com</string>
</array>
结果:App 功能继续正常运行;如果用户拒绝,跟踪会被拦截。
Points of Interest 工具(Xcode 15+)
检测意外的跟踪连接:
- Xcode → Product → Profile
- 选择“Points of Interest”工具
- 运行 App
- 查找显示网络连接的“Privacy”跟踪轨道
- 检查标记出的域名
显示内容:连接到可能用于跨 App/网站跟踪用户的域名的情况。
操作:在 NSPrivacyTrackingDomains 中声明这些域名,或停止连接到这些域名。
第 5 部分:必须说明用途的 API
什么是必须说明用途的 API?
可能被滥用于设备指纹识别(未经许可识别设备)的 API。
任何情况下都不允许设备指纹识别,即使已获得 ATT 权限也不例外。
必须说明用途的 API 有获批的使用场景。你必须声明自己的使用方式适用哪项获批原因。
常见的必须说明用途的 API
| API 类别 | 示例 | 获批原因代码 |
|---|---|---|
| 文件时间戳 | creationDate、modificationDate | C617.1 - DDA9.1 |
| 系统启动时间 | systemUptime、processInfo.systemUptime | 35F9.1、8FFB.1 |
| 磁盘空间 | NSFileSystemFreeSize、volumeAvailableCapacity | E174.1、7D9E.1 |
| 当前键盘 | activeInputModes | 54BD.1、3EC4.1 |
| 用户默认设置 | UserDefaults | CA92.1、1C8F.1、C56D.1 |
示例:磁盘空间 API
API:NSFileSystemFreeSize / URLResourceKey.volumeAvailableCapacityKey
获批原因:
- E174.1:在写入文件前检查空间是否充足
- 7D9E.1:向用户显示存储空间信息
- B728.1:在可选分析中包含磁盘空间信息(仅限用户选择加入时)
在清单中声明:
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>E174.1</string>
</array>
</dict>
</array>
代码:
func checkDiskSpace() -> Bool {
do {
let values = try FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory())
if let freeSpace = values[.systemFreeSize] as? NSNumber {
let requiredSpace: Int64 = 100 * 1024 * 1024 // 100 MB
return freeSpace.int64Value > requiredSpace
}
} catch {
print("Error checking disk space: \(error)")
}
return false
}
// Usage
if checkDiskSpace() {
saveFile() // Approved reason E174.1: Check before writing
} else {
showInsufficientSpaceAlert()
}
示例:UserDefaults API
获批原因:
- CA92.1:访问 App 存储的信息(设置、偏好)
- 1C8F.1:访问 App Group 存储的信息
- C56D.1:访问 App Clips 存储的信息
- AC6B.1:第三方 SDK 访问其自身的默认设置
声明:
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
</array>
</dict>
缺少适用原因时的反馈
如果你的使用场景不在涵盖范围内,请使用 Apple 的反馈表单: https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api
第 6 部分:隐私营养标签
数据类型和类别
标识符:
- 用户 ID
- 设备 ID
联系信息:
- 姓名
- 电子邮件地址
- 电话号码
- 物理地址
位置:
- 精确位置
- 粗略位置
用户内容:
- 照片或视频
- 音频数据
- 游戏内容
- 客户支持消息
浏览历史 搜索历史 财务信息 健康与健身 联系人 敏感信息(种族/族裔数据、政治观点、宗教信仰)
数据使用目的
- App 功能 - 核心功能所必需
- 分析 - 了解 App 使用情况
- 产品个性化 - 定制体验
- 开发者广告 - 为自家产品投放广告
- 第三方广告 - 来自其他公司的广告
与用户关联与未关联
与用户关联:
- 与用户身份相关联的数据(姓名、电子邮件、用户 ID)
- 示例:用户个人资料信息
未与用户关联:
- 与身份无关的数据(匿名分析数据)
- 示例:汇总崩溃报告
跟踪披露
在以下情况下,数据会用于跟踪:
- 与其他 App/网站的数据相结合
- 与数据经纪商共享
- 根据跨 App 行为用于定向广告
声明示例:
Data Type: Email Address
Purpose: App Functionality
Linked to User: Yes
Used for Tracking: No
第 7 部分:Xcode 隐私报告
生成报告
- 将 App 归档:产品 → 归档
- 打开 Xcode Organizer → 选择归档
- 右键点击 → “生成隐私报告”
- 系统会生成 PDF,其中显示汇总的隐私数据
报告包含内容:
- 所有隐私清单(App + 第三方 SDK)
- 收集的数据类型
- 跟踪声明
- 必需原因 API
查看报告
检查以下内容:
- 意料之外的数据收集(SDK 收集了你不知道的数据)
- 缺少必需原因声明
- 跟踪域名不一致
- 缺少隐私清单的第三方 SDK
用途:填写 App Store Connect 中的隐私营养标签
第 8 部分:权限类型
相机
import AVFoundation
AVCaptureDevice.requestAccess(for: .video) { granted in
// Handle response
}
// Info.plist
<key>NSCameraUsageDescription</key>
<string>Take photos of your meals to track nutrition</string>
麦克风
AVAudioSession.sharedInstance().requestRecordPermission { granted in
// Handle response
}
<key>NSMicrophoneUsageDescription</key>
<string>Record voice memos</string>
位置
import CoreLocation
class LocationManager: NSObject, CLLocationManagerDelegate {
let manager = CLLocationManager()
func requestPermission() {
manager.delegate = self
// Choose one:
manager.requestWhenInUseAuthorization() // Only when app is open
// OR
manager.requestAlwaysAuthorization() // Background location
}
}
// Info.plist (iOS 14+)
<key>NSLocationWhenInUseUsageDescription</key>
<string>Show nearby restaurants</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Track your runs even when the app is in the background</string>
照片
import Photos
PHPhotoLibrary.requestAuthorization(for: .readWrite) { status in
switch status {
case .authorized, .limited: // .limited = selected photos only
// Access granted
case .denied, .restricted:
// Access denied
@unknown default:
break
}
}
<key>NSPhotoLibraryUsageDescription</key>
<string>Save and share your workout photos</string>
联系人
import Contacts
CNContactStore().requestAccess(for: .contacts) { granted, error in
// Handle response
}
<key>NSContactsUsageDescription</key>
<string>Invite friends to join you</string>
通知
import UserNotifications
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
// Handle response
}
// No Info.plist entry required
第 9 部分:隐私优先设计模式
数据最小化
原则:只收集实际需要的数据
// ❌ Bad - collecting unnecessary data
struct UserProfile {
let name: String
let email: String
let phone: String // Do you really need this?
let dateOfBirth: Date // Or this?
let socialSecurityNumber: String // Definitely not
}
// ✅ Good - minimal data collection
struct UserProfile {
let name: String
let email: String
// That's it
}
设备端处理
原则:尽可能在本地处理数据
// ✅ Good - on-device ML
import Vision
func analyzePhoto(_ image: UIImage) {
let request = VNClassifyImageRequest { request, error in
// Results stay on device
let classifications = request.results as? [VNClassificationObservation]
self.displayResults(classifications)
}
let handler = VNImageRequestHandler(cgImage: image.cgImage!)
try? handler.perform([request])
// No network request, no data leaving device
}
说明价值交换
原则:清楚说明需要数据的原因
// ✅ Good - clear value proposition
"We use your location to show nearby restaurants and save your favorite places. Your location is never shared with third parties."
透明的数据实践
原则:让用户轻松查看隐私信息
// Add Privacy Policy link in Settings screen
struct SettingsView: View {
var body: some View {
List {
Section("About") {
Link("Privacy Policy", destination: URL(string: "https://example.com/privacy")!)
Link("Data We Collect", destination: URL(string: "https://example.com/data")!)
}
}
}
}
常见错误
App 启动时请求权限
// ❌ Wrong
func application(_ application: UIApplication,
didFinishLaunchingWithOptions...) -> Bool {
requestAllPermissions() // User has no context
return true
}
// ✅ Correct
@objc func cameraButtonTapped() {
requestCameraPermission() // Just-in-time
}
权限对话框出现前未作说明
// ❌ Wrong
AVCaptureDevice.requestAccess(for: .video) { granted in }
// ✅ Correct
showCameraEducation {
AVCaptureDevice.requestAccess(for: .video) { granted in }
}
未妥善处理用户拒绝
// ❌ Wrong - dead end
if !granted {
return // User stuck
}
// ✅ Correct - offer alternative
if !granted {
showSettingsPrompt() // Path forward
}
缺少跟踪域名
// ❌ Wrong - privacy manifest declares tracking but no domains
<key>NSPrivacyTracking</key>
<true/>
// ✅ Correct
<key>NSPrivacyTrackingDomains</key>
<array>
<string>tracking.example.com</string>
</array>
必需原因声明不完整
// ❌ Wrong - using UserDefaults without declaring it
UserDefaults.standard.set(value, forKey: "setting")
// Privacy manifest has no NSPrivacyAccessedAPITypes entry
// ✅ Correct - declared in manifest with approved reason
时间线
| 日期 | 里程碑 |
|---|---|
| WWDC 2023 | 宣布推出隐私清单 |
| 2023 年秋季 | 开始发送提示邮件 |
| 2024 年春季 | 开始在 App 审核中强制执行 |
| 2024 年 5 月 1 日 | 使用会影响隐私的 SDK 的 App 必须提供隐私清单 |
资源
WWDC:2023-10060, 2023-10053
文档:/bundleresources/privacy_manifest_files, /bundleresources/describing-use-of-required-reason-api, /app-store/app-privacy-details, /app-store/user-privacy-and-data-use
技能:axiom-app-intents-ref, axiom-cloudkit-ref, axiom-storage