npx skills add https://github.com/charleswiltgen/axiom --skill axiom-privacy-ux关于隐私优先应用设计的全面指南。Apple 设计奖社会影响力获奖者以合乎道德的方式处理数据,隐私优先设计是关键差异化因素。
隐私清单(PrivacyInfo.xcprivacy)是 Apple 用于透明化数据收集和追踪的框架。结合应用追踪透明度和即时权限请求,它们帮助用户就其数据做出明智的选择。
本技能涵盖创建隐私清单、以出色的用户体验请求系统权限、实施应用追踪透明度、管理追踪域名、使用必需原因 API,以及准备准确的应用隐私标签。
Xcode 导航器 :
PrivacyInfo.xcprivacy广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
文件结构(属性列表):
<?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>
您的应用是否追踪用户?
追踪 = 将来自您应用的用户/设备数据与其他应用/网站的数据结合,以创建用于定向广告或数据代理目的的配置文件。
<key>NSPrivacyTracking</key>
<true/> <!-- or false -->
如果为true,您还必须声明追踪域名:
<key>NSPrivacyTrackingDomains</key>
<array>
<string>tracking.example.com</string>
<string>analytics.example.com</string>
</array>
iOS 17 行为:如果用户未授予 ATT 权限,则对追踪域名的网络请求自动被拦截。
声明应用收集的所有数据:
<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>
常见数据类型:
NSPrivacyCollectedDataTypeName - 用户姓名NSPrivacyCollectedDataTypeEmailAddressNSPrivacyCollectedDataTypePhoneNumberNSPrivacyCollectedDataTypePhysicalAddressNSPrivacyCollectedDataTypePreciseLocationNSPrivacyCollectedDataTypeCoarseLocationNSPrivacyCollectedDataTypePhotosorVideosNSPrivacyCollectedDataTypeContactsNSPrivacyCollectedDataTypeUserID常见用途:
NSPrivacyCollectedDataTypePurposeAppFunctionalityNSPrivacyCollectedDataTypePurposeAnalyticsNSPrivacyCollectedDataTypePurposeProductPersonalizationNSPrivacyCollectedDataTypePurposeDeveloperAdvertisingNSPrivacyCollectedDataTypePurposeThirdPartyAdvertising声明必需原因 API(参见第五部分):
<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>
❌ 不要:在启动时请求所有权限
// 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: "拍摄照片",
message: "FoodSnap 需要相机权限才能让您拍摄餐食照片并获取营养信息。",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "继续", style: .default) { _ in
completion() // Now request actual permission
})
alert.addAction(UIAlertAction(title: "暂不", style: .cancel))
present(alert, animated: true)
}
为什么这有效:
永远不要让用户陷入死胡同:
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: "需要相机权限",
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)
}
打开特定的设置屏幕:
// General app settings
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
// Notification settings (iOS 15.4+)
UIApplication.shared.open(URL(string: UIApplication.openNotificationSettingsURLString)!)
如果出现以下情况,您必须请求 ATT 权限:
如果仅出现以下情况,则不需要 ATT:
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()
}
}
}
}
Info.plist:
<key>NSUserTrackingUsageDescription</key>
<string>This allows us to show you personalized ads and improve your experience</string>
最佳实践:
❌ 错误示例:
✅ 良好示例:
在 ATT 系统提示之前显示您自己的对话框:
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%。
始终提供无追踪的价值:
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()
}
}
在 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> <!-- Declared, will be blocked -->
</array>
结果:应用功能继续工作;如果被拒绝,追踪则被拦截。
检测意外的追踪连接:
它显示的内容:连接到可能跨应用/网站追踪用户的域名。
操作:在 NSPrivacyTrackingDomains 中声明这些域名,或停止连接到它们。
可能被滥用于设备指纹识别(未经许可识别设备)的 API。
设备指纹识别是绝对不允许的,即使拥有 ATT 权限。
必需原因 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:NSFileSystemFreeSize / URLResourceKey.volumeAvailableCapacityKey
批准原因:
在清单中的声明:
<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>
代码:
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()
}
批准原因:
声明:
<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
标识符:
联系信息:
位置:
用户内容:
浏览历史 搜索历史 财务信息 健康与健身 联系人 敏感信息(种族/民族数据、政治观点、宗教信仰)
与用户关联:
不与用户关联:
数据用于追踪,如果:
示例声明:
Data Type: Email Address
Purpose: App Functionality
Linked to User: Yes
Used for Tracking: No
包含内容:
检查:
用于:在 App Store Connect 中完成应用隐私标签
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
原则:仅收集实际需要的数据
// ❌ 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")!)
}
}
}
}
// ❌ 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/>
<!-- Missing NSPrivacyTrackingDomains -->
// ✅ 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 年春季 | 开始应用审核强制执行 |
| 2024 年 5 月 1 日 | 对具有隐私影响 SDK 的应用要求隐私清单 |
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
每周安装次数
92
仓库
GitHub 星标数
601
首次出现
2026 年 1 月 21 日
安全审计
安装于
opencode77
claude-code73
codex71
gemini-cli69
cursor68
github-copilot65
Comprehensive guide to privacy-first app design. Apple Design Award Social Impact winners handle data ethically, and privacy-first design is a key differentiator.
Privacy manifests (PrivacyInfo.xcprivacy) 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.
This 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.
Xcode Navigator :
PrivacyInfo.xcprivacyFile structure (Property List):
<?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>
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.
<key>NSPrivacyTracking</key>
<true/> <!-- or false -->
Iftrue, you must also declare tracking domains:
<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.
Declare all data your app collects:
<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 :
NSPrivacyCollectedDataTypeName - User's nameNSPrivacyCollectedDataTypeEmailAddressNSPrivacyCollectedDataTypePhoneNumberNSPrivacyCollectedDataTypePhysicalAddressNSPrivacyCollectedDataTypePreciseLocationNSPrivacyCollectedDataTypeCoarseLocationNSPrivacyCollectedDataTypePhotosorVideosNSPrivacyCollectedDataTypeContactsNSPrivacyCollectedDataTypeUserIDCommon purposes :
NSPrivacyCollectedDataTypePurposeAppFunctionalityNSPrivacyCollectedDataTypePurposeAnalyticsNSPrivacyCollectedDataTypePurposeProductPersonalizationNSPrivacyCollectedDataTypePurposeDeveloperAdvertisingNSPrivacyCollectedDataTypePurposeThirdPartyAdvertisingDeclare Required Reason APIs (see Part 5):
<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>
❌ Don't : Request all permissions at launch
// 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
// 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()
}
}
}
}
Explain why you need permission before showing system dialog:
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 :
Never dead-end the user :
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)
}
Open specific settings screens:
// General app settings
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
// Notification settings (iOS 15.4+)
UIApplication.shared.open(URL(string: UIApplication.openNotificationSettingsURLString)!)
You must request ATT permission if you:
You don't need ATT if you only :
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()
}
}
}
}
Info.plist :
<key>NSUserTrackingUsageDescription</key>
<string>This allows us to show you personalized ads and improve your experience</string>
Best practices :
❌ Bad examples :
✅ Good examples :
Show your own dialog before ATT system prompt:
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%.
Always provide value without tracking :
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()
}
}
In PrivacyInfo.xcprivacy:
<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.
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 :
<key>NSPrivacyTrackingDomains</key>
<array>
<string>tracking.example.com</string> <!-- Declared, will be blocked -->
</array>
Result: App functionality continues working; tracking blocked if denied.
Detecting unexpected tracking connections :
What it shows : Connections to domains that may be tracking users across apps/websites.
Action : Declare these domains in NSPrivacyTrackingDomains or stop connecting to them.
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 Category | Examples | Approved Reason Codes |
|---|---|---|
| File timestamp | creationDate, modificationDate | C617.1 - DDA9.1 |
| System boot time | systemUptime, processInfo.systemUptime | 35F9.1, 8FFB.1 |
API : NSFileSystemFreeSize / URLResourceKey.volumeAvailableCapacityKey
Approved reasons :
Declaration in manifest :
<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 :
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()
}
Approved reasons :
Declaration :
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
</array>
</dict>
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
Identifiers :
Contact Info :
Location :
User Content :
Browsing History Search History Financial Info Health & Fitness Contacts Sensitive Info (racial/ethnic data, political opinions, religious beliefs)
Linked to user :
Not linked to user :
Data is used for tracking if:
Example declaration :
Data Type: Email Address
Purpose: App Functionality
Linked to User: Yes
Used for Tracking: No
What's included :
Check for :
Use for : Completing Privacy Nutrition Labels in App Store Connect
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
Principle : Only collect data you actually need
// ❌ 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
}
Principle : Process data locally when possible
// ✅ 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
}
Principle : Be transparent about why you need data
// ✅ 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."
Principle : Make privacy information easily accessible
// 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")!)
}
}
}
}
// ❌ 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/>
<!-- Missing NSPrivacyTrackingDomains -->
// ✅ 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
| 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-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
Weekly Installs
92
Repository
GitHub Stars
601
First Seen
Jan 21, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode77
claude-code73
codex71
gemini-cli69
cursor68
github-copilot65
Flutter布局指南:构建响应式UI的约束规则与自适应设计模式
1,200 周安装
| Disk space | NSFileSystemFreeSize, volumeAvailableCapacity | E174.1, 7D9E.1 |
| Active keyboards | activeInputModes | 54BD.1, 3EC4.1 |
| User defaults | UserDefaults | CA92.1, 1C8F.1, C56D.1 |