Files of iOS Privacy & Permissions
wondelai/
Show the full text456 lines
iOS Privacy & Permissions
Best practices for permission requests, privacy UI, and building user trust.
Table of Contents
- Permission Request Philosophy
- Permission Request Timing
- Permission Types & Best Practices
- Handling Denied Permissions
- Privacy UI Patterns
- App Privacy Labels
- Usage String Best Practices
- Testing Permissions
Permission Request Philosophy
Core principle: Request permissions only when needed, explain why, and respect "no."
Users are increasingly permission-fatigued. Every unnecessary or poorly-timed request damages trust and increases denial rates.
Permission Request Timing
Just-In-Time Requests
Request permissions when the user takes an action that requires them, not at app launch.
Bad: Request camera permission on first launch Good: Request camera permission when user taps "Take Photo"
The Pre-Permission Pattern
Before the system dialog, show a custom screen explaining the value.
┌─────────────────────────────────────────┐
│ │
│ [Camera icon] │
│ │
│ Take photos of your receipts │
│ │
│ We use your camera to quickly │
│ scan and organize your expenses. │
│ Photos are stored only on your │
│ device. │
│ │
│ ┌─────────────────────────────────┐ │
│ │ Allow Camera │ │
│ └─────────────────────────────────┘ │
│ │
│ Maybe Later │
│ │
└─────────────────────────────────────────┘
Benefits:
- Explains value before system dialog
- "Maybe Later" doesn't trigger system denial
- Higher acceptance rates
- Better user understanding
System Permission Dialog
// Camera
AVCaptureDevice.requestAccess(for: .video) { granted in
// Handle response
}
// Photos
PHPhotoLibrary.requestAuthorization(for: .readWrite) { status in
// Handle status
}
// Location
locationManager.requestWhenInUseAuthorization()
// or
locationManager.requestAlwaysAuthorization()
// Notifications
UNUserNotificationCenter.current().requestAuthorization(
options: [.alert, .badge, .sound]
) { granted, error in
// Handle response
}
Permission Types & Best Practices
Camera
When to request: When user initiates camera action
Usage string example: "[App] needs camera access to scan documents and take photos for your projects."
Best practices:
- Only request when camera feature is used
- Offer photo library as alternative
- Handle denial gracefully (show library option)
Photo Library
Access levels (iOS 14+):
.addOnly- Can add photos, can't read (for saving).readWrite- Full access- Limited selection - User picks specific photos
When to request: When user wants to access photos
Usage string example: "[App] accesses your photos to let you add images to your posts."
Best practices:
- Request
.addOnlyif you only need to save - Support limited photo selection (don't require full access)
- Use PHPicker for one-time selection (no permission needed)
// PHPicker - no permission required
var config = PHPickerConfiguration()
config.selectionLimit = 1
config.filter = .images
let picker = PHPickerViewController(configuration: config)
Location
Authorization levels:
.whenInUse- Only while app is active.always- Background location access
When to request: When location feature is needed
Usage strings needed:
NSLocationWhenInUseUsageDescriptionNSLocationAlwaysAndWhenInUseUsageDescription(for always)
Best practices:
- Start with "When In Use" before requesting "Always"
- Explain why background location is needed
- Provide value even without location
- Use significant location changes if precise tracking unnecessary
// Request when in use first
locationManager.requestWhenInUseAuthorization()
// Later, if needed, escalate to always
// (triggers new system prompt explaining upgrade)
locationManager.requestAlwaysAuthorization()
Notifications
When to request: After user has experienced app value
Usage string example: "[App] sends notifications for messages from your team and important updates."
Best practices:
- Don't request on first launch
- Wait until user has seen value
- Explain what notifications they'll receive
- Provide in-app notification preferences
- Respect system settings
Provisional notifications (iOS 12+):
// Quietly delivered to Notification Center
// User can choose to keep or turn off
UNUserNotificationCenter.current().requestAuthorization(
options: [.provisional, .alert, .sound]
) { granted, error in }
Contacts
When to request: When user initiates contact-related feature
Usage string example: "[App] accesses your contacts to help you invite friends and find people you know."
Best practices:
- Use CNContactPickerViewController when possible (no permission)
- Only request full access when truly needed
- Never sync contacts without explicit permission
Microphone
When to request: When user initiates audio recording
Usage string example: "[App] uses your microphone to record voice messages and audio notes."
Best practices:
- Clear indicator when recording
- Option to preview before sending
- Explain storage/transmission of audio
Health Data
When to request: When user enables health features
Best practices:
- Request only specific data types needed
- Explain how data will be used
- Provide value without health access
- Handle partial authorization
Tracking (ATT)
When to request: Before tracking user across apps
Required prompt:
ATTrackingManager.requestTrackingAuthorization { status in
switch status {
case .authorized:
// Enable tracking
case .denied, .restricted:
// Disable tracking
case .notDetermined:
// Request hasn't been shown yet
}
}
Best practices:
- Explain value of personalized ads first
- Don't punish users who decline
- App must function without tracking
Handling Denied Permissions
Graceful Degradation
Always provide alternative paths when permission is denied.
| Permission Denied | Alternative |
|---|---|
| Camera | Photo library option |
| Location | Manual address entry |
| Notifications | In-app message center |
| Contacts | Manual contact entry |
| Photos | Camera-only option |
Re-Requesting After Denial
Once denied, system won't show prompt again. Guide users to Settings.
func openAppSettings() {
guard let settingsUrl = URL(string: UIApplication.openSettingsURLString),
UIApplication.shared.canOpenURL(settingsUrl) else {
return
}
UIApplication.shared.open(settingsUrl)
}
UI pattern:
┌─────────────────────────────────────────┐
│ │
│ Camera Access Needed │
│ │
│ To scan documents, allow camera │
│ access in Settings. │
│ │
│ ┌─────────────────────────────────┐ │
│ │ Open Settings │ │
│ └─────────────────────────────────┘ │
│ │
│ Not Now │
│ │
└─────────────────────────────────────────┘
Don't Ask Again (Within Session)
If user taps "Maybe Later" on pre-permission screen, don't immediately ask again.
// Track dismissal
UserDefaults.standard.set(Date(), forKey: "camera_prompt_dismissed")
// Wait before showing again
func shouldShowCameraPrompt() -> Bool {
guard let lastDismissed = UserDefaults.standard.object(
forKey: "camera_prompt_dismissed"
) as? Date else {
return true
}
// Wait at least 3 days
return Date().timeIntervalSince(lastDismissed) > 3 * 24 * 60 * 60
}
Privacy UI Patterns
Permission Status Indicators
Show current permission state in settings:
┌─────────────────────────────────────────┐
│ Permissions │
├─────────────────────────────────────────┤
│ 📷 Camera Allowed ▶ │
│ 📍 Location While Using▶ │
│ 🔔 Notifications Off ▶ │
│ 📱 Contacts Not Asked ▶ │
└─────────────────────────────────────────┘
Data Usage Transparency
Explain what data is collected and why:
┌─────────────────────────────────────────┐
│ Privacy │
├─────────────────────────────────────────┤
│ │
│ Data We Collect │
│ │
│ • Usage analytics │
│ To improve app performance │
│ │
│ • Crash reports │
│ To fix bugs and issues │
│ │
│ • Photos you upload │
│ Stored securely on our servers │
│ │
│ [View Privacy Policy] │
│ │
└─────────────────────────────────────────┘
Data Deletion Options
Provide clear data management:
┌─────────────────────────────────────────┐
│ Your Data │
├─────────────────────────────────────────┤
│ Download My Data ▶ │
│ Delete My Account ▶ │
└─────────────────────────────────────────┘
App Privacy Labels
Required Categories
Your App Store listing must declare:
Data Used to Track You
- Data used for advertising across apps
Data Linked to You
- Identifiable data (name, email, etc.)
Data Not Linked to You
- Anonymous analytics, crash data
Best Practices
- Be accurate—Apple verifies
- Minimize collection to reduce label size
- Simpler labels build trust
- Update when collection changes
Usage String Best Practices
Structure
"[App name] [action] to [user benefit]."
Examples by Permission
| Permission | Good Example |
|---|---|
| Camera | "MyApp uses the camera to scan barcodes for quick product lookup." |
| Photos | "MyApp saves photos you create to your photo library." |
| Location | "MyApp uses your location to show nearby restaurants and estimated delivery times." |
| Microphone | "MyApp uses the microphone to record voice notes for your journal entries." |
| Contacts | "MyApp accesses contacts to help you split bills with friends." |
What to Avoid
- Generic explanations ("to improve your experience")
- Technical jargon
- Mentioning advertising without explaining value
- Being vague about data use
Testing Permissions
Reset Permissions
# Reset all permissions for specific app
xcrun simctl privacy booted reset all com.yourapp.bundleid
# Reset specific permission
xcrun simctl privacy booted reset camera com.yourapp.bundleid
Test All States
For each permission:
- Never requested (first launch)
- Authorized
- Denied
- Restricted (parental controls)
- Limited (Photos)
- Provisional (Notifications)
Automated Testing
func testCameraPermissionDenied() {
// Set up mock authorization status
mockCameraAuthorization = .denied
// Trigger camera feature
app.buttons["Take Photo"].tap()
// Verify fallback UI appears
XCTAssertTrue(app.staticTexts["Camera access needed"].exists)
XCTAssertTrue(app.buttons["Open Settings"].exists)
}
| 1 | # iOS Privacy & Permissions |
| 2 | |
| 3 | Best practices for permission requests, privacy UI, and building user trust. |
| 4 | |
| 5 | |
| 6 | ## Table of Contents |
| 7 | [Permission Request Philosophy] |
| 8 | [Permission Request Timing] |
| 9 | [Permission Types & Best Practices] |
| 10 | [Handling Denied Permissions] |
| 11 | [Privacy UI Patterns] |
| 12 | [App Privacy Labels] |
| 13 | [Usage String Best Practices] |
| 14 | [Testing Permissions] |
| 15 | |
| 16 | |
| 17 | |
| 18 | ## Permission Request Philosophy |
| 19 | |
| 20 | **Core principle:** Request permissions only when needed, explain why, and respect "no." |
| 21 | |
| 22 | Users are increasingly permission-fatigued. Every unnecessary or poorly-timed request damages trust and increases denial rates. |
| 23 | |
| 24 | |
| 25 | |
| 26 | ## Permission Request Timing |
| 27 | |
| 28 | ### Just-In-Time Requests |
| 29 | |
| 30 | Request permissions when the user takes an action that requires them, not at app launch. |
| 31 | |
| 32 | **Bad:** Request camera permission on first launch |
| 33 | **Good:** Request camera permission when user taps "Take Photo" |
| 34 | |
| 35 | ### The Pre-Permission Pattern |
| 36 | |
| 37 | Before the system dialog, show a custom screen explaining the value. |
| 38 | |
| 39 | |
| 40 | ┌─────────────────────────────────────────┐ |
| 41 | │ │ |
| 42 | │ [Camera icon] │ |
| 43 | │ │ |
| 44 | │ Take photos of your receipts │ |
| 45 | │ │ |
| 46 | │ We use your camera to quickly │ |
| 47 | │ scan and organize your expenses. │ |
| 48 | │ Photos are stored only on your │ |
| 49 | │ device. │ |
| 50 | │ │ |
| 51 | │ ┌─────────────────────────────────┐ │ |
| 52 | │ │ Allow Camera │ │ |
| 53 | │ └─────────────────────────────────┘ │ |
| 54 | │ │ |
| 55 | │ Maybe Later │ |
| 56 | │ │ |
| 57 | └─────────────────────────────────────────┘ |
| 58 | |
| 59 | |
| 60 | **Benefits:** |
| 61 | Explains value before system dialog |
| 62 | "Maybe Later" doesn't trigger system denial |
| 63 | Higher acceptance rates |
| 64 | Better user understanding |
| 65 | |
| 66 | ### System Permission Dialog |
| 67 | |
| 68 | |
| 69 | // Camera |
| 70 | AVCaptureDevice.requestAccess(for: .video) { granted in |
| 71 | // Handle response |
| 72 | } |
| 73 | |
| 74 | // Photos |
| 75 | PHPhotoLibrary.requestAuthorization(for: .readWrite) { status in |
| 76 | // Handle status |
| 77 | } |
| 78 | |
| 79 | // Location |
| 80 | locationManager.requestWhenInUseAuthorization() |
| 81 | // or |
| 82 | locationManager.requestAlwaysAuthorization() |
| 83 | |
| 84 | // Notifications |
| 85 | UNUserNotificationCenter.current().requestAuthorization( |
| 86 | options: [.alert, .badge, .sound] |
| 87 | ) { granted, error in |
| 88 | // Handle response |
| 89 | } |
| 90 | |
| 91 | |
| 92 | |
| 93 | |
| 94 | ## Permission Types & Best Practices |
| 95 | |
| 96 | ### Camera |
| 97 | |
| 98 | **When to request:** When user initiates camera action |
| 99 | |
| 100 | **Usage string example:** |
| 101 | "[App] needs camera access to scan documents and take photos for your projects." |
| 102 | |
| 103 | **Best practices:** |
| 104 | Only request when camera feature is used |
| 105 | Offer photo library as alternative |
| 106 | Handle denial gracefully (show library option) |
| 107 | |
| 108 | ### Photo Library |
| 109 | |
| 110 | **Access levels (iOS 14+):** |
| 111 | `.addOnly` - Can add photos, can't read (for saving) |
| 112 | `.readWrite` - Full access |
| 113 | Limited selection - User picks specific photos |
| 114 | |
| 115 | **When to request:** When user wants to access photos |
| 116 | |
| 117 | **Usage string example:** |
| 118 | "[App] accesses your photos to let you add images to your posts." |
| 119 | |
| 120 | **Best practices:** |
| 121 | Request `.addOnly` if you only need to save |
| 122 | Support limited photo selection (don't require full access) |
| 123 | Use PHPicker for one-time selection (no permission needed) |
| 124 | |
| 125 | |
| 126 | // PHPicker - no permission required |
| 127 | var config = PHPickerConfiguration() |
| 128 | config.selectionLimit = 1 |
| 129 | config.filter = .images |
| 130 | |
| 131 | let picker = PHPickerViewController(configuration: config) |
| 132 | |
| 133 | |
| 134 | ### Location |
| 135 | |
| 136 | **Authorization levels:** |
| 137 | `.whenInUse` - Only while app is active |
| 138 | `.always` - Background location access |
| 139 | |
| 140 | **When to request:** When location feature is needed |
| 141 | |
| 142 | **Usage strings needed:** |
| 143 | `NSLocationWhenInUseUsageDescription` |
| 144 | `NSLocationAlwaysAndWhenInUseUsageDescription` (for always) |
| 145 | |
| 146 | **Best practices:** |
| 147 | Start with "When In Use" before requesting "Always" |
| 148 | Explain why background location is needed |
| 149 | Provide value even without location |
| 150 | Use significant location changes if precise tracking unnecessary |
| 151 | |
| 152 | |
| 153 | // Request when in use first |
| 154 | locationManager.requestWhenInUseAuthorization() |
| 155 | |
| 156 | // Later, if needed, escalate to always |
| 157 | // (triggers new system prompt explaining upgrade) |
| 158 | locationManager.requestAlwaysAuthorization() |
| 159 | |
| 160 | |
| 161 | ### Notifications |
| 162 | |
| 163 | **When to request:** After user has experienced app value |
| 164 | |
| 165 | **Usage string example:** |
| 166 | "[App] sends notifications for messages from your team and important updates." |
| 167 | |
| 168 | **Best practices:** |
| 169 | Don't request on first launch |
| 170 | Wait until user has seen value |
| 171 | Explain what notifications they'll receive |
| 172 | Provide in-app notification preferences |
| 173 | Respect system settings |
| 174 | |
| 175 | **Provisional notifications (iOS 12+):** |
| 176 | |
| 177 | // Quietly delivered to Notification Center |
| 178 | // User can choose to keep or turn off |
| 179 | UNUserNotificationCenter.current().requestAuthorization( |
| 180 | options: [.provisional, .alert, .sound] |
| 181 | ) { granted, error in } |
| 182 | |
| 183 | |
| 184 | ### Contacts |
| 185 | |
| 186 | **When to request:** When user initiates contact-related feature |
| 187 | |
| 188 | **Usage string example:** |
| 189 | "[App] accesses your contacts to help you invite friends and find people you know." |
| 190 | |
| 191 | **Best practices:** |
| 192 | Use CNContactPickerViewController when possible (no permission) |
| 193 | Only request full access when truly needed |
| 194 | Never sync contacts without explicit permission |
| 195 | |
| 196 | ### Microphone |
| 197 | |
| 198 | **When to request:** When user initiates audio recording |
| 199 | |
| 200 | **Usage string example:** |
| 201 | "[App] uses your microphone to record voice messages and audio notes." |
| 202 | |
| 203 | **Best practices:** |
| 204 | Clear indicator when recording |
| 205 | Option to preview before sending |
| 206 | Explain storage/transmission of audio |
| 207 | |
| 208 | ### Health Data |
| 209 | |
| 210 | **When to request:** When user enables health features |
| 211 | |
| 212 | **Best practices:** |
| 213 | Request only specific data types needed |
| 214 | Explain how data will be used |
| 215 | Provide value without health access |
| 216 | Handle partial authorization |
| 217 | |
| 218 | ### Tracking (ATT) |
| 219 | |
| 220 | **When to request:** Before tracking user across apps |
| 221 | |
| 222 | **Required prompt:** |
| 223 | |
| 224 | ATTrackingManager.requestTrackingAuthorization { status in |
| 225 | switch status { |
| 226 | case .authorized: |
| 227 | // Enable tracking |
| 228 | case .denied, .restricted: |
| 229 | // Disable tracking |
| 230 | case .notDetermined: |
| 231 | // Request hasn't been shown yet |
| 232 | } |
| 233 | } |
| 234 | |
| 235 | |
| 236 | **Best practices:** |
| 237 | Explain value of personalized ads first |
| 238 | Don't punish users who decline |
| 239 | App must function without tracking |
| 240 | |
| 241 | |
| 242 | |
| 243 | ## Handling Denied Permissions |
| 244 | |
| 245 | ### Graceful Degradation |
| 246 | |
| 247 | Always provide alternative paths when permission is denied. |
| 248 | |
| 249 | | Permission Denied | Alternative | |
| 250 | |-------------------|-------------| |
| 251 | | Camera | Photo library option | |
| 252 | | Location | Manual address entry | |
| 253 | | Notifications | In-app message center | |
| 254 | | Contacts | Manual contact entry | |
| 255 | | Photos | Camera-only option | |
| 256 | |
| 257 | ### Re-Requesting After Denial |
| 258 | |
| 259 | Once denied, system won't show prompt again. Guide users to Settings. |
| 260 | |
| 261 | |
| 262 | func openAppSettings() { |
| 263 | guard let settingsUrl = URL(string: UIApplication.openSettingsURLString), |
| 264 | UIApplication.shared.canOpenURL(settingsUrl) else { |
| 265 | return |
| 266 | } |
| 267 | UIApplication.shared.open(settingsUrl) |
| 268 | } |
| 269 | |
| 270 | |
| 271 | **UI pattern:** |
| 272 | |
| 273 | ┌─────────────────────────────────────────┐ |
| 274 | │ │ |
| 275 | │ Camera Access Needed │ |
| 276 | │ │ |
| 277 | │ To scan documents, allow camera │ |
| 278 | │ access in Settings. │ |
| 279 | │ │ |
| 280 | │ ┌─────────────────────────────────┐ │ |
| 281 | │ │ Open Settings │ │ |
| 282 | │ └─────────────────────────────────┘ │ |
| 283 | │ │ |
| 284 | │ Not Now │ |
| 285 | │ │ |
| 286 | └─────────────────────────────────────────┘ |
| 287 | |
| 288 | |
| 289 | ### Don't Ask Again (Within Session) |
| 290 | |
| 291 | If user taps "Maybe Later" on pre-permission screen, don't immediately ask again. |
| 292 | |
| 293 | |
| 294 | // Track dismissal |
| 295 | UserDefaults.standard.set(Date(), forKey: "camera_prompt_dismissed") |
| 296 | |
| 297 | // Wait before showing again |
| 298 | func shouldShowCameraPrompt() -> Bool { |
| 299 | guard let lastDismissed = UserDefaults.standard.object( |
| 300 | forKey: "camera_prompt_dismissed" |
| 301 | ) as? Date else { |
| 302 | return true |
| 303 | } |
| 304 | // Wait at least 3 days |
| 305 | return Date().timeIntervalSince(lastDismissed) > 3 * 24 * 60 * 60 |
| 306 | } |
| 307 | |
| 308 | |
| 309 | |
| 310 | |
| 311 | ## Privacy UI Patterns |
| 312 | |
| 313 | ### Permission Status Indicators |
| 314 | |
| 315 | Show current permission state in settings: |
| 316 | |
| 317 | |
| 318 | ┌─────────────────────────────────────────┐ |
| 319 | │ Permissions │ |
| 320 | ├─────────────────────────────────────────┤ |
| 321 | │ 📷 Camera Allowed ▶ │ |
| 322 | │ 📍 Location While Using▶ │ |
| 323 | │ 🔔 Notifications Off ▶ │ |
| 324 | │ 📱 Contacts Not Asked ▶ │ |
| 325 | └─────────────────────────────────────────┘ |
| 326 | |
| 327 | |
| 328 | ### Data Usage Transparency |
| 329 | |
| 330 | Explain what data is collected and why: |
| 331 | |
| 332 | |
| 333 | ┌─────────────────────────────────────────┐ |
| 334 | │ Privacy │ |
| 335 | ├─────────────────────────────────────────┤ |
| 336 | │ │ |
| 337 | │ Data We Collect │ |
| 338 | │ │ |
| 339 | │ • Usage analytics │ |
| 340 | │ To improve app performance │ |
| 341 | │ │ |
| 342 | │ • Crash reports │ |
| 343 | │ To fix bugs and issues │ |
| 344 | │ │ |
| 345 | │ • Photos you upload │ |
| 346 | │ Stored securely on our servers │ |
| 347 | │ │ |
| 348 | │ [View Privacy Policy] │ |
| 349 | │ │ |
| 350 | └─────────────────────────────────────────┘ |
| 351 | |
| 352 | |
| 353 | ### Data Deletion Options |
| 354 | |
| 355 | Provide clear data management: |
| 356 | |
| 357 | |
| 358 | ┌─────────────────────────────────────────┐ |
| 359 | │ Your Data │ |
| 360 | ├─────────────────────────────────────────┤ |
| 361 | │ Download My Data ▶ │ |
| 362 | │ Delete My Account ▶ │ |
| 363 | └─────────────────────────────────────────┘ |
| 364 | |
| 365 | |
| 366 | |
| 367 | |
| 368 | ## App Privacy Labels |
| 369 | |
| 370 | ### Required Categories |
| 371 | |
| 372 | Your App Store listing must declare: |
| 373 | |
| 374 | **Data Used to Track You** |
| 375 | Data used for advertising across apps |
| 376 | |
| 377 | **Data Linked to You** |
| 378 | Identifiable data (name, email, etc.) |
| 379 | |
| 380 | **Data Not Linked to You** |
| 381 | Anonymous analytics, crash data |
| 382 | |
| 383 | ### Best Practices |
| 384 | |
| 385 | Be accurate—Apple verifies |
| 386 | Minimize collection to reduce label size |
| 387 | Simpler labels build trust |
| 388 | Update when collection changes |
| 389 | |
| 390 | |
| 391 | |
| 392 | ## Usage String Best Practices |
| 393 | |
| 394 | ### Structure |
| 395 | |
| 396 | |
| 397 | "[App name] [action] to [user benefit]." |
| 398 | |
| 399 | |
| 400 | ### Examples by Permission |
| 401 | |
| 402 | | Permission | Good Example | |
| 403 | |------------|--------------| |
| 404 | | Camera | "MyApp uses the camera to scan barcodes for quick product lookup." | |
| 405 | | Photos | "MyApp saves photos you create to your photo library." | |
| 406 | | Location | "MyApp uses your location to show nearby restaurants and estimated delivery times." | |
| 407 | | Microphone | "MyApp uses the microphone to record voice notes for your journal entries." | |
| 408 | | Contacts | "MyApp accesses contacts to help you split bills with friends." | |
| 409 | |
| 410 | ### What to Avoid |
| 411 | |
| 412 | Generic explanations ("to improve your experience") |
| 413 | Technical jargon |
| 414 | Mentioning advertising without explaining value |
| 415 | Being vague about data use |
| 416 | |
| 417 | |
| 418 | |
| 419 | ## Testing Permissions |
| 420 | |
| 421 | ### Reset Permissions |
| 422 | |
| 423 | |
| 424 | # Reset all permissions for specific app |
| 425 | xcrun simctl privacy booted reset all com.yourapp.bundleid |
| 426 | |
| 427 | # Reset specific permission |
| 428 | xcrun simctl privacy booted reset camera com.yourapp.bundleid |
| 429 | |
| 430 | |
| 431 | ### Test All States |
| 432 | |
| 433 | For each permission: |
| 434 | Never requested (first launch) |
| 435 | Authorized |
| 436 | Denied |
| 437 | Restricted (parental controls) |
| 438 | Limited (Photos) |
| 439 | Provisional (Notifications) |
| 440 | |
| 441 | ### Automated Testing |
| 442 | |
| 443 | |
| 444 | func testCameraPermissionDenied() { |
| 445 | // Set up mock authorization status |
| 446 | mockCameraAuthorization = .denied |
| 447 | |
| 448 | // Trigger camera feature |
| 449 | app.buttons["Take Photo"].tap() |
| 450 | |
| 451 | // Verify fallback UI appears |
| 452 | XCTAssertTrue(app.staticTexts["Camera access needed"].exists) |
| 453 | XCTAssertTrue(app.buttons["Open Settings"].exists) |
| 454 | } |
| 455 | |
| 456 |
Discussion
Browse more free Claude skills.