axiom-privacy-ux について
Appleプラットフォームの開発者向けに、プライバシーマニフェスト、システム権限、ATT、トラッキングドメイン、Required Reason APIを扱うUXスキルです。App Storeのプライバシーラベルや審査上の問題も取り上げます。
主な機能
- 01PrivacyInfo.xcprivacyマニフェストの作成
- 02必要な場面での権限リクエスト設計
- 03App Tracking Transparencyの実装
- 04トラッキングドメインとRequired Reason APIの管理
- 05App 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を入力して読み込まれたか確認します。
詳細
プライバシー UX パターン
プライバシーを最優先するアプリ設計の包括的なガイドです。Apple Design Award の社会的影響部門の受賞者はデータを倫理的に扱っており、プライバシーを最優先する設計は重要な差別化要因です。
概要
プライバシーマニフェスト(PrivacyInfo.xcprivacy)は、データ収集とトラッキングに関する透明性を確保するための Apple のフレームワークです。App Tracking Transparency や、必要なタイミングでの権限リクエストと組み合わせることで、ユーザーが自分のデータについて十分な情報に基づいて選択できるようになります。
このスキルでは、プライバシーマニフェストの作成、優れた UX を備えたシステム権限のリクエスト、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 フレームワーク)に追加
ファイル構造(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>
</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:権限リクエストの UX
必要なタイミングでのリクエストと事前リクエスト
❌ してはいけないこと:起動時にすべての権限をリクエストする
// 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が必要な場合
次の場合は、必ずATTの許可を求める必要があります。
- 他社が所有するアプリやウェブサイトを横断してユーザーを追跡する
- ユーザーデータをデータブローカーと共有する
- 追跡を行うサードパーティ製SDKを使用する(Facebook SDK、Google Analyticsなど)
次の場合のみであれば、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文)
❌ 悪い例:
- 「お客様のプライバシーを大切にしています」(曖昧)
- 「アプリを動作させるために必要です」(不誠実)
- 「アプリを収益化するためです」(ユーザーには関係ありません)
✅ 良い例:
- 「興味を持ちそうな商品の関連広告を表示するのに役立ちます」
- 「広告をパーソナライズすることで、このアプリを無料で提供できます」
追跡前のプロンプト設計
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>
結果:アプリの機能は引き続き動作し、許可されていない場合は追跡がブロックされます。
Points of Interestインストゥルメント(Xcode 15以降)
予期しない追跡接続を検出するには:
- Xcode → Product → Profile
- 「Points of Interest」インストゥルメントを選択する
- アプリを実行する
- ネットワーク接続が表示される「Privacy」トラックを確認する
- フラグが付いたドメインを確認する
表示内容:アプリやウェブサイトを横断してユーザーを追跡している可能性のあるドメインへの接続です。
対応:これらのドメインを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:アプリが保存した情報(設定、環境設定)にアクセスする
- 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
連絡先情報:
- 名前
- メールアドレス
- 電話番号
- 住所
位置情報:
- 正確な位置情報
- おおよその位置情報
ユーザーコンテンツ:
- 写真または動画
- 音声データ
- ゲームプレイのコンテンツ
- カスタマーサポートのメッセージ
閲覧履歴 検索履歴 財務情報 健康とフィットネス 連絡先 機微情報(人種・民族に関するデータ、政治的見解、宗教的信条)
データの利用目的
- アプリの機能 - 中核機能に必要です
- 分析 - アプリの利用状況を把握します
- 製品のパーソナライズ - 利用体験をカスタマイズします
- 開発者による広告 - 自社製品の広告です
- サードパーティ広告 - 他社の広告です
ユーザーとの関連付け
ユーザーに関連付けられるデータ:
- ユーザーの識別情報(名前、メールアドレス、ユーザー ID)に関連付けられたデータ
- 例:ユーザープロフィール情報
ユーザーに関連付けられないデータ:
- 識別情報に関連付けられていないデータ(匿名の分析データ)
- 例:集計されたクラッシュレポート
トラッキングの開示
次の場合、データはトラッキングに使用されます:
- 他のアプリやウェブサイトのデータと組み合わせる場合
- データブローカーと共有する場合
- 複数のアプリにまたがる行動に基づくターゲット広告に使用する場合
宣言の例:
Data Type: Email Address
Purpose: App Functionality
Linked to User: Yes
Used for Tracking: No
パート 7:Xcode プライバシーレポート
レポートの生成
- アプリをアーカイブします:Product → Archive
- Xcode Organizer → アーカイブを選択します
- 右クリック → 「Generate Privacy Report」
- プライバシーデータを集計した PDF が作成されます
含まれる内容:
- すべてのプライバシーマニフェスト(アプリ + サードパーティ SDK)
- 収集されるデータの種類
- トラッキングの宣言
- Required Reason API
レポートの確認
確認事項:
- 想定外のデータ収集(知らないうちに SDK がデータを収集していないか)
- Required Reason の宣言漏れ
- トラッキングドメインの不一致
- プライバシーマニフェストのないサードパーティ 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")!)
}
}
}
}
よくある間違い
アプリ起動時に権限を要求する
// ❌ 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>
Required Reason の宣言が不完全
// ❌ 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 | プライバシーマニフェストが発表される |
| Fall 2023 | 情報提供メールの送信が始まる |
| Spring 2024 | App Review での適用が始まる |
| May 1, 2024 | プライバシーに影響する SDK を含むアプリにプライバシーマニフェストが必須となる |
リソース
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