axiom-privacy-ux
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePrivacy UX Patterns
隐私用户体验模式
Comprehensive guide to privacy-first app design. Apple Design Award Social Impact winners handle data ethically, and privacy-first design is a key differentiator.
本指南全面介绍隐私优先的应用设计思路。获得苹果设计奖社会影响力奖项的应用都在以符合伦理的方式处理数据,而隐私优先设计是关键的差异化优势。
Overview
概述
Privacy manifests () are Apple's framework for transparency about data collection and tracking. Combined with App Tracking Transparency and just-in-time permission requests, they help users make informed choices about their data.
PrivacyInfo.xcprivacyThis skill covers creating privacy manifests, requesting system permissions with excellent UX, implementing App Tracking Transparency, managing tracking domains, using Required Reason APIs, and preparing accurate Privacy Nutrition Labels.
隐私清单()是苹果推出的框架,用于透明展示数据收集和跟踪行为。结合App Tracking Transparency和即时权限请求机制,能帮助用户就其数据使用做出明智选择。
PrivacyInfo.xcprivacy本内容涵盖创建隐私清单、以优质用户体验请求系统权限、实现App Tracking Transparency、管理跟踪域名、使用Required Reason API以及准备准确的隐私营养标签等内容。
When to Use This Skill
适用场景
- Creating privacy manifests (PrivacyInfo.xcprivacy)
- Requesting system permissions (Camera, Location, etc.)
- Implementing App Tracking Transparency (ATT)
- Preparing Privacy Nutrition Labels for App Store Connect
- Managing tracking domains to avoid accidental tracking
- Using Required Reason APIs (NSFileSystemFreeSize, UserDefaults, etc.)
- Explaining data usage to users transparently
- Debugging privacy-related App Store rejections
- 创建隐私清单(PrivacyInfo.xcprivacy)
- 请求系统权限(相机、位置等)
- 实现App Tracking Transparency(ATT)
- 为App Store Connect准备隐私营养标签
- 管理跟踪域名以避免意外跟踪
- 使用Required Reason API(NSFileSystemFreeSize、UserDefaults等)
- 向用户透明解释数据使用方式
- 调试与隐私相关的App Store审核驳回问题
System Requirements
系统要求
- iOS 14.5+ for App Tracking Transparency
- iOS 17+ for automatic tracking domain blocking
- Xcode 15+ for privacy reports and manifest editing
- Spring 2024+ for App Review enforcement
- iOS 14.5+:支持App Tracking Transparency
- iOS 17+:支持自动跟踪域名拦截
- Xcode 15+:支持隐私报告和清单编辑
- 2024年春季起:App Store审核强制要求相关隐私配置
Part 1: Privacy Manifests (WWDC 2023/10060)
第一部分:隐私清单(WWDC 2023/10060)
Creating a Privacy Manifest
创建隐私清单
Xcode Navigator:
- File → New → File
- Choose "App Privacy File"
- Name:
PrivacyInfo.xcprivacy - Add to app target (or SDK framework)
File structure (Property List):
xml
<?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>
<!-- Data types collected -->
</array>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<!-- Required Reason APIs used -->
</array>
</dict>
</plist>Xcode导航操作:
- 文件 → 新建 → 文件
- 选择“App Privacy File”
- 命名:
PrivacyInfo.xcprivacy - 添加到应用目标(或SDK框架)
文件结构(属性列表):
xml
<?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>
<!-- Data types collected -->
</array>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<!-- Required Reason APIs used -->
</array>
</dict>
</plist>NSPrivacyTracking Declaration
NSPrivacyTracking声明
Does your app track users?
Tracking = combining user/device data from your app with data from other apps/websites to create a profile for targeted advertising or data broker purposes.
xml
<key>NSPrivacyTracking</key>
<true/> <!-- or false -->If , you must also declare tracking domains:
truexml
<key>NSPrivacyTrackingDomains</key>
<array>
<string>tracking.example.com</string>
<string>analytics.example.com</string>
</array>iOS 17 behavior: Network requests to tracking domains automatically blocked if user hasn't granted ATT permission.
你的应用是否跟踪用户?
跟踪行为指:将你的应用中的用户/设备数据与其他应用/网站的数据结合,创建用户档案用于定向广告或数据经纪目的。
xml
<key>NSPrivacyTracking</key>
<true/> <!-- 或false -->如果设为,你还必须声明跟踪域名:
truexml
<key>NSPrivacyTrackingDomains</key>
<array>
<string>tracking.example.com</string>
<string>analytics.example.com</string>
</array>iOS 17行为:若用户未授予ATT权限,向跟踪域名发起的网络请求会被自动拦截。
NSPrivacyCollectedDataTypes
NSPrivacyCollectedDataTypes
Declare all data your app collects:
xml
<key>NSPrivacyCollectedDataTypes</key>
<array>
<dict>
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypeName</string>
<key>NSPrivacyCollectedDataTypeLinked</key>
<true/> <!-- Linked to user identity? -->
<key>NSPrivacyCollectedDataTypeTracking</key>
<false/> <!-- Used for tracking? -->
<key>NSPrivacyCollectedDataTypePurposes</key>
<array>
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
<string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
</array>
</dict>
</array>Common data types:
- - User's name
NSPrivacyCollectedDataTypeName NSPrivacyCollectedDataTypeEmailAddressNSPrivacyCollectedDataTypePhoneNumberNSPrivacyCollectedDataTypePhysicalAddressNSPrivacyCollectedDataTypePreciseLocationNSPrivacyCollectedDataTypeCoarseLocationNSPrivacyCollectedDataTypePhotosorVideosNSPrivacyCollectedDataTypeContactsNSPrivacyCollectedDataTypeUserID
Common purposes:
NSPrivacyCollectedDataTypePurposeAppFunctionalityNSPrivacyCollectedDataTypePurposeAnalyticsNSPrivacyCollectedDataTypePurposeProductPersonalizationNSPrivacyCollectedDataTypePurposeDeveloperAdvertisingNSPrivacyCollectedDataTypePurposeThirdPartyAdvertising
声明应用收集的所有数据类型:
xml
<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 - - 邮箱地址
NSPrivacyCollectedDataTypeEmailAddress - - 电话号码
NSPrivacyCollectedDataTypePhoneNumber - - 物理地址
NSPrivacyCollectedDataTypePhysicalAddress - - 精确位置
NSPrivacyCollectedDataTypePreciseLocation - - 粗略位置
NSPrivacyCollectedDataTypeCoarseLocation - - 照片或视频
NSPrivacyCollectedDataTypePhotosorVideos - - 联系人
NSPrivacyCollectedDataTypeContacts - - 用户ID
NSPrivacyCollectedDataTypeUserID
常见用途:
- - 应用功能
NSPrivacyCollectedDataTypePurposeAppFunctionality - - 数据分析
NSPrivacyCollectedDataTypePurposeAnalytics - - 产品个性化
NSPrivacyCollectedDataTypePurposeProductPersonalization - - 开发者广告
NSPrivacyCollectedDataTypePurposeDeveloperAdvertising - - 第三方广告
NSPrivacyCollectedDataTypePurposeThirdPartyAdvertising
NSPrivacyAccessedAPITypes
NSPrivacyAccessedAPITypes
Declare Required Reason APIs (see Part 5):
xml
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>C617.1</string> <!-- Approved reason code -->
</array>
</dict>
</array>声明使用的Required Reason API(见第五部分):
xml
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>C617.1</string> <!-- 已批准的原因代码 -->
</array>
</dict>
</array>Part 2: Permission Request UX
第二部分:权限请求用户体验
Just-in-Time vs Up-Front
即时请求 vs 提前请求
❌ Don't: Request all permissions at launch
swift
// BAD - overwhelming and confusing
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
requestCameraPermission()
requestLocationPermission()
requestNotificationPermission()
requestPhotoLibraryPermission()
return true
}✅ Do: Request just-in-time when user triggers feature
swift
// 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()
}
}
}
}❌ 错误做法: 应用启动时请求所有权限
swift
// BAD - overwhelming and confusing
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
requestCameraPermission()
requestLocationPermission()
requestNotificationPermission()
requestPhotoLibraryPermission()
return true
}✅ 正确做法: 用户触发功能时即时请求
swift
// GOOD - clear causality
@objc func takePhotoButtonTapped() {
// 先展示权限前置说明
showCameraEducation {
// 再请求权限
AVCaptureDevice.requestAccess(for: .video) { granted in
if granted {
self.openCamera()
} else {
self.showPermissionDeniedAlert()
}
}
}
}Pre-Permission Education Screens
权限前置说明界面
Explain why you need permission before showing system dialog:
swift
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)
}Why this works:
- User understands value proposition
- System dialog rejection rate drops 60-80%
- Better App Store ratings (fewer "why does it need that?" reviews)
在展示系统权限弹窗之前,解释你需要权限的原因:
swift
func showCameraEducation(completion: @escaping () -> Void) {
let alert = UIAlertController(
title: "拍摄照片",
message: "FoodSnap需要相机权限来让你拍摄餐食并获取营养信息。",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "继续", style: .default) { _ in
completion() // 现在请求实际权限
})
alert.addAction(UIAlertAction(title: "稍后再说", style: .cancel))
present(alert, animated: true)
}为什么有效:
- 用户理解价值主张
- 系统弹窗拒绝率降低60-80%
- App Store评分更高(减少“为什么需要这个权限?”的差评)
Permission Denied Handling
权限被拒绝的处理
Never dead-end the user:
swift
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)
}永远不要让用户陷入死胡同:
swift
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() // 提供打开设置的选项
@unknown default:
break
}
}
func showSettingsPrompt() {
let alert = UIAlertController(
title: "需要相机权限",
message: "请在设置中启用相机权限以使用此功能。",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "打开设置", style: .default) { _ in
if let url = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url)
}
})
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
present(alert, animated: true)
}Settings Deep Links
设置深度链接
Open specific settings screens:
swift
// General app settings
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
// Notification settings (iOS 15.4+)
UIApplication.shared.open(URL(string: UIApplication.openNotificationSettingsURLString)!)打开特定设置界面:
swift
// 通用应用设置
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
// 通知设置(iOS 15.4+)
UIApplication.shared.open(URL(string: UIApplication.openNotificationSettingsURLString)!)Part 3: App Tracking Transparency
第三部分:App Tracking Transparency
When ATT Is Required
何时需要ATT
You must request ATT permission if you:
- Track users across apps/websites owned by other companies
- Share user data with data brokers
- Use third-party SDKs that track (Facebook SDK, Google Analytics, etc.)
You don't need ATT if you only:
- Use first-party analytics (no sharing with other companies)
- Personalize ads based only on data from your own app
- Use fraud detection/security measures
当你有以下行为时,必须请求ATT权限:
- 跨其他公司拥有的应用/网站跟踪用户
- 与数据经纪商共享用户数据
- 使用会跟踪用户的第三方SDK(Facebook SDK、Google Analytics等)
当你仅有以下行为时,不需要ATT:
- 使用第一方数据分析(不与其他公司共享)
- 仅基于自身应用数据个性化广告
- 使用欺诈检测/安全措施
ATTrackingManager.requestTrackingAuthorization
ATTrackingManager.requestTrackingAuthorization
swift
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()
}
}
}
}swift
import AppTrackingTransparency
import AdSupport
func requestTrackingPermission() {
// 检查兼容性(iOS 14.5+)
guard #available(iOS 14.5, *) else { return }
// 等待应用激活后再请求
// 过早展示弹窗会导致自动拒绝
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
ATTrackingManager.requestTrackingAuthorization { status in
switch status {
case .authorized:
// 用户授予权限
// 现在可以访问IDFA并进行跟踪
let idfa = ASIdentifierManager.shared().advertisingIdentifier
self.initializeTrackingSDKs(idfa: idfa)
case .denied:
// 用户拒绝权限
// 禁止进行跟踪
self.initializeNonTrackingSDKs()
case .notDetermined:
// 用户关闭弹窗未选择
// 视为拒绝
self.initializeNonTrackingSDKs()
case .restricted:
// 设备不允许跟踪(家长控制)
self.initializeNonTrackingSDKs()
@unknown default:
self.initializeNonTrackingSDKs()
}
}
}
}Custom ATT Prompt Message
自定义ATT提示文案
Info.plist:
xml
<key>NSUserTrackingUsageDescription</key>
<string>This allows us to show you personalized ads and improve your experience</string>Best practices:
- Be honest and specific
- Explain user benefit (not company benefit)
- Keep it concise (1-2 sentences)
❌ Bad examples:
- "We value your privacy" (vague)
- "This is required for the app to work" (dishonest)
- "To monetize our app" (user doesn't care)
✅ Good examples:
- "This helps us show you relevant ads for products you might like"
- "Personalized ads help keep this app free"
Info.plist:
xml
<key>NSUserTrackingUsageDescription</key>
<string>这能让我们为你展示个性化广告并提升使用体验</string>最佳实践:
- 诚实且具体
- 解释对用户的好处(而非公司的好处)
- 保持简洁(1-2句话)
❌ 错误示例:
- "我们重视你的隐私"(模糊)
- "应用运行需要此权限"(不诚实)
- "用于应用变现"(用户不关心)
✅ 正确示例:
- "这能帮助我们为你展示可能感兴趣的相关广告"
- "个性化广告助力我们保持应用免费"
Pre-Tracking Prompt Design
跟踪前置提示设计
Show your own dialog before ATT system prompt:
swift
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)
}Why this works: Education increases opt-in rates by 20-40%.
在ATT系统弹窗前展示自定义提示:
swift
func showPreTrackingPrompt() {
let alert = UIAlertController(
title: "支持免费功能",
message: "我们使用跟踪功能展示个性化广告,这有助于维持高级功能的免费使用。你随时可以在设置中更改此选项。",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "继续", style: .default) { _ in
self.requestTrackingPermission()
})
alert.addAction(UIAlertAction(title: "稍后再说", style: .cancel))
present(alert, animated: true)
}为什么有效:前置说明能让同意率提升20-40%。
Graceful Degradation
优雅降级处理
Always provide value without tracking:
swift
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()
}
}即使没有跟踪权限,也要为用户提供价值:
swift
func initializeAnalytics() {
let status = ATTrackingManager.trackingAuthorizationStatus
if status == .authorized {
// 全功能分析
Analytics.setUserProperty(userID, forName: "user_id")
Analytics.enableCrossAppTracking()
} else {
// 有限的隐私保护型分析
Analytics.setUserProperty("anonymous", forName: "user_id")
Analytics.disableCrossAppTracking()
Analytics.enableOnDeviceConversionTracking()
}
}Part 4: Tracking Domain Management
第四部分:跟踪域名管理
Declaring Tracking Domains
声明跟踪域名
In :
PrivacyInfo.xcprivacyxml
<key>NSPrivacyTracking</key>
<true/>
<key>NSPrivacyTrackingDomains</key>
<array>
<string>tracking.example.com</string>
<string>ads.example.com</string>
</array>iOS 17 behavior: If user denies ATT, network requests to these domains are automatically blocked.
在中:
PrivacyInfo.xcprivacyxml
<key>NSPrivacyTracking</key>
<true/>
<key>NSPrivacyTrackingDomains</key>
<array>
<string>tracking.example.com</string>
<string>ads.example.com</string>
</array>iOS 17行为:若用户拒绝ATT权限,向这些域名发起的网络请求会被自动拦截。
Domain Separation Strategy
域名分离策略
Problem: Single domain used for both tracking and non-tracking
Solution: Separate functionality into different hosts
Before:
- api.example.com (mixed tracking + app functionality)
After:
- api.example.com (app functionality only)
- tracking.example.com (tracking only)Update manifest:
xml
<key>NSPrivacyTrackingDomains</key>
<array>
<string>tracking.example.com</string> <!-- Declared, will be blocked -->
</array>Result: App functionality continues working; tracking blocked if denied.
问题:单个域名同时用于跟踪和非跟踪功能
解决方案:将功能分离到不同主机
优化前:
- api.example.com(混合跟踪 + 应用功能)
优化后:
- api.example.com(仅应用功能)
- tracking.example.com(仅跟踪)更新清单:
xml
<key>NSPrivacyTrackingDomains</key>
<array>
<string>tracking.example.com</string> <!-- 已声明,会被拦截 -->
</array>结果:应用功能正常运行;若用户拒绝权限,跟踪功能会被拦截。
Points of Interest Instrument (Xcode 15+)
兴趣点工具(Xcode 15+)
Detecting unexpected tracking connections:
- Xcode → Product → Profile
- Choose "Points of Interest" instrument
- Run app
- Look for "Privacy" track showing network connections
- Review flagged domains
What it shows: Connections to domains that may be tracking users across apps/websites.
Action: Declare these domains in or stop connecting to them.
NSPrivacyTrackingDomains检测意外跟踪连接:
- Xcode → 产品 → 性能分析
- 选择“Points of Interest”工具
- 运行应用
- 查看“Privacy”轨道显示的网络连接
- 检查标记的域名
展示内容:与跨应用/网站跟踪用户的域名的连接情况。
操作:在中声明这些域名,或停止连接这些域名。
NSPrivacyTrackingDomainsPart 5: Required Reason APIs
第五部分:Required Reason API
What Are Required Reason APIs?
什么是Required Reason API?
APIs that could be misused for fingerprinting (identifying devices without permission).
Fingerprinting is never allowed, even with ATT permission.
Required Reason APIs have approved use cases. You must declare which approved reason applies to your usage.
可能被滥用于设备指纹识别(未经权限识别设备)的API。
设备指纹识别是绝对不允许的,即使获得ATT权限也不行。
Required Reason API有已批准的使用场景。你必须声明你的使用场景对应的已批准原因。
Common Required Reason APIs
常见Required Reason API
| API Category | Examples | Approved Reason Codes |
|---|---|---|
| File timestamp | | |
| System boot time | | |
| Disk space | | |
| Active keyboards | | |
| User defaults | | |
| API类别 | 示例 | 已批准原因代码 |
|---|---|---|
| 文件时间戳 | | |
| 系统启动时间 | | |
| 磁盘空间 | | |
| 活跃键盘 | | |
| 用户默认设置 | | |
Example: Disk Space API
示例:磁盘空间API
API: /
NSFileSystemFreeSizeURLResourceKey.volumeAvailableCapacityKeyApproved reasons:
- E174.1: Check if there's enough space before writing files
- 7D9E.1: Display storage information to user
- B728.1: Include disk space in optional analytics (only if user opted in)
Declaration in manifest:
xml
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>E174.1</string> <!-- Check space before writing -->
</array>
</dict>
</array>Code:
swift
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()
}API: /
NSFileSystemFreeSizeURLResourceKey.volumeAvailableCapacityKey已批准原因:
- E174.1:写入文件前检查是否有足够空间
- 7D9E.1:向用户显示存储信息
- B728.1:在可选分析中包含磁盘空间信息(仅当用户选择加入时)
清单中的声明:
xml
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>E174.1</string> <!-- 写入前检查空间 -->
</array>
</dict>
</array>代码:
swift
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
}
// 使用示例
if checkDiskSpace() {
saveFile() // 已批准原因E174.1:写入前检查
} else {
showInsufficientSpaceAlert()
}Example: UserDefaults API
示例:UserDefaults API
Approved reasons:
- CA92.1: Access info stored by app (settings, preferences)
- 1C8F.1: Access info stored by App Group
- C56D.1: Access info stored by App Clips
- AC6B.1: Third-party SDK accessing its own defaults
Declaration:
xml
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
</array>
</dict>已批准原因:
- CA92.1:访问应用存储的信息(设置、偏好)
- 1C8F.1:访问App Group存储的信息
- C56D.1:访问App Clips存储的信息
- AC6B.1:第三方SDK访问自身默认设置
声明:
xml
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
</array>
</dict>Feedback for Missing Reasons
缺失原因的反馈
If your use case isn't covered, use Apple's feedback form:
https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api
Part 6: Privacy Nutrition Labels
第六部分:隐私营养标签
Data Types and Categories
数据类型和类别
Identifiers:
- User ID
- Device ID
Contact Info:
- Name
- Email address
- Phone number
- Physical address
Location:
- Precise location
- Coarse location
User Content:
- Photos or videos
- Audio data
- Gameplay content
- Customer support messages
Browsing History
Search History
Financial Info
Health & Fitness
Contacts
Sensitive Info (racial/ethnic data, political opinions, religious beliefs)
标识符:
- 用户ID
- 设备ID
联系信息:
- 姓名
- 邮箱地址
- 电话号码
- 物理地址
位置:
- 精确位置
- 粗略位置
用户内容:
- 照片或视频
- 音频数据
- 游戏内容
- 客户支持消息
浏览历史
搜索历史
财务信息
健康与健身
联系人
敏感信息(种族/民族数据、政治观点、宗教信仰)
Data Use Purposes
数据使用目的
- App functionality - Necessary for core features
- Analytics - Understanding app usage
- Product personalization - Customizing experience
- Developer advertising - Ads for your own products
- Third-party advertising - Ads from other companies
- 应用功能 - 核心功能必需
- 数据分析 - 了解应用使用情况
- 产品个性化 - 定制使用体验
- 开发者广告 - 自家产品广告
- 第三方广告 - 其他公司的广告
Linked vs Not Linked
关联用户 vs 不关联用户
Linked to user:
- Data connected to user identity (name, email, user ID)
- Example: User profile information
Not linked to user:
- Data not connected to identity (anonymous analytics)
- Example: Aggregate crash reports
关联用户:
- 数据与用户身份关联(姓名、邮箱、用户ID)
- 示例:用户档案信息
不关联用户:
- 数据不与身份关联(匿名分析)
- 示例:聚合崩溃报告
Tracking Disclosure
跟踪披露
Data is used for tracking if:
- Combined with data from other apps/websites
- Shared with data brokers
- Used for targeted advertising based on cross-app behavior
Example declaration:
Data Type: Email Address
Purpose: App Functionality
Linked to User: Yes
Used for Tracking: No如果数据用于以下场景,则属于跟踪用途:
- 与其他应用/网站的数据结合
- 与数据经纪商共享
- 基于跨应用行为用于定向广告
示例声明:
数据类型:邮箱地址
用途:应用功能
关联用户:是
用于跟踪:否Part 7: Xcode Privacy Report
第七部分:Xcode隐私报告
Generating Report
生成报告
- Archive app: Product → Archive
- Xcode Organizer → Select archive
- Right-click → "Generate Privacy Report"
- PDF created showing aggregated privacy data
What's included:
- All privacy manifests (app + third-party SDKs)
- Collected data types
- Tracking declaration
- Required Reason APIs
- 归档应用:产品 → 归档
- Xcode管理器 → 选择归档文件
- 右键 → "生成隐私报告"
- 创建PDF文件,展示聚合的隐私数据
包含内容:
- 所有隐私清单(应用 + 第三方SDK)
- 收集的数据类型
- 跟踪声明
- Required Reason API
Reviewing Report
审核报告
Check for:
- Unexpected data collection (SDK collecting data you didn't know about)
- Missing Required Reason declarations
- Tracking domain discrepancies
- Third-party SDKs without privacy manifests
Use for: Completing Privacy Nutrition Labels in App Store Connect
检查要点:
- 意外的数据收集(SDK收集了你不知道的数据)
- 缺失的Required Reason声明
- 跟踪域名不一致
- 无隐私清单的第三方SDK
用途:完成App Store Connect中的隐私营养标签填写
Part 8: Permission Types
第八部分:权限类型
Camera
相机
swift
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>swift
import AVFoundation
AVCaptureDevice.requestAccess(for: .video) { granted in
// 处理响应
}
// Info.plist
<key>NSCameraUsageDescription</key>
<string>拍摄餐食照片以跟踪营养</string>Microphone
麦克风
swift
AVAudioSession.sharedInstance().requestRecordPermission { granted in
// Handle response
}
<key>NSMicrophoneUsageDescription</key>
<string>Record voice memos</string>swift
AVAudioSession.sharedInstance().requestRecordPermission { granted in
// 处理响应
}
<key>NSMicrophoneUsageDescription</key>
<string>录制语音备忘录</string>Location
位置
swift
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>swift
import CoreLocation
class LocationManager: NSObject, CLLocationManagerDelegate {
let manager = CLLocationManager()
func requestPermission() {
manager.delegate = self
// 选择其中一种:
manager.requestWhenInUseAuthorization() // 仅应用打开时
// 或
manager.requestAlwaysAuthorization() // 后台位置
}
}
// Info.plist (iOS 14+)
<key>NSLocationWhenInUseUsageDescription</key>
<string>展示附近餐厅</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>即使应用在后台也能跟踪你的跑步</string>Photos
照片
swift
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>swift
import Photos
PHPhotoLibrary.requestAuthorization(for: .readWrite) { status in
switch status {
case .authorized, .limited: // .limited = 仅选中的照片
// 权限已授予
case .denied, .restricted:
// 权限被拒绝
@unknown default:
break
}
}
<key>NSPhotoLibraryUsageDescription</key>
<string>保存和分享你的健身照片</string>Contacts
联系人
swift
import Contacts
CNContactStore().requestAccess(for: .contacts) { granted, error in
// Handle response
}
<key>NSContactsUsageDescription</key>
<string>Invite friends to join you</string>swift
import Contacts
CNContactStore().requestAccess(for: .contacts) { granted, error in
// 处理响应
}
<key>NSContactsUsageDescription</key>
<string>邀请朋友加入</string>Notifications
通知
swift
import UserNotifications
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
// Handle response
}
// No Info.plist entry requiredswift
import UserNotifications
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
// 处理响应
}
// 无需在Info.plist中添加条目Part 9: Privacy-First Design Patterns
第九部分:隐私优先设计模式
Data Minimization
数据最小化
Principle: Only collect data you actually need
swift
// ❌ 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
}原则:仅收集实际需要的数据
swift
// ❌ 错误 - 收集不必要的数据
struct UserProfile {
let name: String
let email: String
let phone: String // 你真的需要这个吗?
let dateOfBirth: Date // 或者这个?
let socialSecurityNumber: String // 绝对不需要
}
// ✅ 正确 - 最小化数据收集
struct UserProfile {
let name: String
let email: String
// 这样就够了
}On-Device Processing
本地设备处理
Principle: Process data locally when possible
swift
// ✅ 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
}原则:尽可能在本地处理数据
swift
// ✅ 正确 - 本地设备机器学习
import Vision
func analyzePhoto(_ image: UIImage) {
let request = VNClassifyImageRequest { request, error in
// 结果保留在本地设备
let classifications = request.results as? [VNClassificationObservation]
self.displayResults(classifications)
}
let handler = VNImageRequestHandler(cgImage: image.cgImage!)
try? handler.perform([request])
// 无网络请求,数据不会离开设备
}Explaining Value Exchange
价值交换说明
Principle: Be transparent about why you need data
swift
// ✅ 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."原则:透明说明你需要数据的原因
swift
// ✅ 正确 - 清晰的价值主张
"我们使用你的位置展示附近餐厅并保存你喜欢的地点。你的位置绝不会与第三方共享。"Transparent Data Practices
透明的数据实践
Principle: Make privacy information easily accessible
swift
// 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")!)
}
}
}
}原则:让隐私信息易于获取
swift
// 在设置界面添加隐私政策链接
struct SettingsView: View {
var body: some View {
List {
Section("关于") {
Link("隐私政策", destination: URL(string: "https://example.com/privacy")!)
Link("我们收集的数据", destination: URL(string: "https://example.com/data")!)
}
}
}
}Common Mistakes
常见错误
Requesting permissions at launch
应用启动时请求权限
swift
// ❌ Wrong
func application(_ application: UIApplication,
didFinishLaunchingWithOptions...) -> Bool {
requestAllPermissions() // User has no context
return true
}
// ✅ Correct
@objc func cameraButtonTapped() {
requestCameraPermission() // Just-in-time
}swift
// ❌ 错误
func application(_ application: UIApplication,
didFinishLaunchingWithOptions...) -> Bool {
requestAllPermissions() // 用户没有上下文
return true
}
// ✅ 正确
@objc func cameraButtonTapped() {
requestCameraPermission() // 即时请求
}No explanation before permission dialog
权限弹窗前无说明
swift
// ❌ Wrong
AVCaptureDevice.requestAccess(for: .video) { granted in }
// ✅ Correct
showCameraEducation {
AVCaptureDevice.requestAccess(for: .video) { granted in }
}swift
// ❌ 错误
AVCaptureDevice.requestAccess(for: .video) { granted in }
// ✅ 正确
showCameraEducation {
AVCaptureDevice.requestAccess(for: .video) { granted in }
}Not handling denial gracefully
未优雅处理权限拒绝
swift
// ❌ Wrong - dead end
if !granted {
return // User stuck
}
// ✅ Correct - offer alternative
if !granted {
showSettingsPrompt() // Path forward
}swift
// ❌ 错误 - 死胡同
if !granted {
return // 用户陷入困境
}
// ✅ 正确 - 提供替代方案
if !granted {
showSettingsPrompt() // 提供解决路径
}Missing tracking domains
缺失跟踪域名
swift
// ❌ Wrong - privacy manifest declares tracking but no domains
<key>NSPrivacyTracking</key>
<true/>
<!-- Missing NSPrivacyTrackingDomains -->
// ✅ Correct
<key>NSPrivacyTrackingDomains</key>
<array>
<string>tracking.example.com</string>
</array>swift
// ❌ 错误 - 隐私清单声明了跟踪但未填写域名
<key>NSPrivacyTracking</key>
<true/>
<!-- 缺失NSPrivacyTrackingDomains -->
// ✅ 正确
<key>NSPrivacyTrackingDomains</key>
<array>
<string>tracking.example.com</string>
</array>Incomplete Required Reason declarations
Required Reason声明不完整
swift
// ❌ Wrong - using UserDefaults without declaring it
UserDefaults.standard.set(value, forKey: "setting")
// Privacy manifest has no NSPrivacyAccessedAPITypes entry
// ✅ Correct - declared in manifest with approved reasonswift
// ❌ 错误 - 使用UserDefaults但未声明
UserDefaults.standard.set(value, forKey: "setting")
// 隐私清单中无NSPrivacyAccessedAPITypes条目
// ✅ 正确 - 在清单中声明并填写已批准原因Timeline
时间线
| Date | Milestone |
|---|---|
| WWDC 2023 | Privacy manifests announced |
| Fall 2023 | Informational emails begin |
| Spring 2024 | App Review enforcement begins |
| May 1, 2024 | Privacy manifests required for apps with privacy-impacting SDKs |
| 日期 | 里程碑 |
|---|---|
| WWDC 2023 | 隐私清单发布 |
| 2023年秋季 | 开始发送提示邮件 |
| 2024年春季 | App Store审核开始强制执行 |
| 2024年5月1日 | 对包含隐私影响SDK的应用强制要求隐私清单 |
Resources
资源
WWDC: 2023-10060, 2023-10053
Docs: /bundleresources/privacy_manifest_files, /bundleresources/describing-use-of-required-reason-api, /app-store/app-privacy-details, /app-store/user-privacy-and-data-use
Skills: axiom-app-intents-ref, axiom-cloudkit-ref, axiom-storage
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