Loading...
Loading...
Use when implementing privacy manifests, requesting permissions, App Tracking Transparency UX, or preparing Privacy Nutrition Labels - covers just-in-time permission requests, tracking domain management, and Required Reason APIs from WWDC 2023
npx skill4agent add charleswiltgen/axiom axiom-privacy-uxPrivacyInfo.xcprivacyPrivacyInfo.xcprivacy<?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><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>NSPrivacyCollectedDataTypeNameNSPrivacyCollectedDataTypeEmailAddressNSPrivacyCollectedDataTypePhoneNumberNSPrivacyCollectedDataTypePhysicalAddressNSPrivacyCollectedDataTypePreciseLocationNSPrivacyCollectedDataTypeCoarseLocationNSPrivacyCollectedDataTypePhotosorVideosNSPrivacyCollectedDataTypeContactsNSPrivacyCollectedDataTypeUserIDNSPrivacyCollectedDataTypePurposeAppFunctionalityNSPrivacyCollectedDataTypePurposeAnalyticsNSPrivacyCollectedDataTypePurposeProductPersonalizationNSPrivacyCollectedDataTypePurposeDeveloperAdvertisingNSPrivacyCollectedDataTypePurposeThirdPartyAdvertising<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: "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)
}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)!)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()
}
}
}
}<key>NSUserTrackingUsageDescription</key>
<string>This allows us to show you personalized ads and improve your experience</string>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)
}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>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 Category | Examples | Approved Reason Codes |
|---|---|---|
| File timestamp | | |
| System boot time | | |
| Disk space | | |
| Active keyboards | | |
| User defaults | | |
NSFileSystemFreeSizeURLResourceKey.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>Data Type: Email Address
Purpose: App Functionality
Linked to User: Yes
Used for Tracking: Noimport 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| 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 |