mcpField
A
axiom-privacy-ux

axiom-privacy-ux

github.com/charleswiltgen安全1.2k174 次安装今天更新
代码仓库

关于 axiom-privacy-ux

面向 Apple 平台开发者的隐私体验设计技能,说明隐私清单、系统权限、ATT、追踪域名和必需理由 API 的处理方式。还涵盖 App Store 隐私标签及隐私相关审核问题。

主要功能

  1. 01编写 PrivacyInfo.xcprivacy 清单
  2. 02设计适时的系统权限请求
  3. 03实施 App Tracking Transparency
  4. 04管理追踪域名与必需理由 API
  5. 05准备 App Store 隐私标签

适用场景

  1. 01为应用声明收集的数据和 API 用途
  2. 02在功能触发时请求相机或定位权限
  3. 03检查追踪连接及 ATT 行为
  4. 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 导航器:

  1. File → New → File
  2. 选择“App Privacy File”
  3. 名称:PrivacyInfo.xcprivacy
  4. 添加到应用目标(或 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 - 用户姓名
  • NSPrivacyCollectedDataTypeEmailAddress
  • NSPrivacyCollectedDataTypePhoneNumber
  • NSPrivacyCollectedDataTypePhysicalAddress
  • NSPrivacyCollectedDataTypePreciseLocation
  • NSPrivacyCollectedDataTypeCoarseLocation
  • NSPrivacyCollectedDataTypePhotosorVideos
  • NSPrivacyCollectedDataTypeContacts
  • NSPrivacyCollectedDataTypeUserID

常见用途:

  • NSPrivacyCollectedDataTypePurposeAppFunctionality
  • NSPrivacyCollectedDataTypePurposeAnalytics
  • NSPrivacyCollectedDataTypePurposeProductPersonalization
  • NSPrivacyCollectedDataTypePurposeDeveloperAdvertising
  • NSPrivacyCollectedDataTypePurposeThirdPartyAdvertising
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+)

检测意外的跟踪连接:

  1. Xcode → Product → Profile
  2. 选择“Points of Interest”工具
  3. 运行 App
  4. 查找显示网络连接的“Privacy”跟踪轨道
  5. 检查标记出的域名

显示内容:连接到可能用于跨 App/网站跟踪用户的域名的情况。

操作:在 NSPrivacyTrackingDomains 中声明这些域名,或停止连接到这些域名。


第 5 部分:必须说明用途的 API

什么是必须说明用途的 API?

可能被滥用于设备指纹识别(未经许可识别设备)的 API。

任何情况下都不允许设备指纹识别,即使已获得 ATT 权限也不例外。

必须说明用途的 API 有获批的使用场景。你必须声明自己的使用方式适用哪项获批原因。

常见的必须说明用途的 API
API 类别示例获批原因代码
文件时间戳creationDate、modificationDateC617.1 - DDA9.1
系统启动时间systemUptime、processInfo.systemUptime35F9.1、8FFB.1
磁盘空间NSFileSystemFreeSize、volumeAvailableCapacityE174.1、7D9E.1
当前键盘activeInputModes54BD.1、3EC4.1
用户默认设置UserDefaultsCA92.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 隐私报告

生成报告
  1. 将 App 归档:产品 → 归档
  2. 打开 Xcode Organizer → 选择归档
  3. 右键点击 → “生成隐私报告”
  4. 系统会生成 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

评论

支持 Markdown