iOS Privacy & Permissions skill

Best practices for permission requests, privacy UI, and building user trust.

by wondelai·MIT license·★ 2,235 Stars on the repo·GitHub ↗

Use now

Files of iOS Privacy & Permissions

wondelai/main1 file
privacy-permissions.md
Show the full text456 lines

iOS Privacy & Permissions

Best practices for permission requests, privacy UI, and building user trust.

Table of Contents

  1. Permission Request Philosophy
  2. Permission Request Timing
  3. Permission Types & Best Practices
  4. Handling Denied Permissions
  5. Privacy UI Patterns
  6. App Privacy Labels
  7. Usage String Best Practices
  8. 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 .addOnly if 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:

  • NSLocationWhenInUseUsageDescription
  • NSLocationAlwaysAndWhenInUseUsageDescription (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:

  1. Never requested (first launch)
  2. Authorized
  3. Denied
  4. Restricted (parental controls)
  5. Limited (Photos)
  6. 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 
3Best practices for permission requests, privacy UI, and building user trust.
4 
5 
6## Table of Contents
71. [Permission Request Philosophy](#permission-request-philosophy)
82. [Permission Request Timing](#permission-request-timing)
93. [Permission Types & Best Practices](#permission-types-best-practices)
104. [Handling Denied Permissions](#handling-denied-permissions)
115. [Privacy UI Patterns](#privacy-ui-patterns)
126. [App Privacy Labels](#app-privacy-labels)
137. [Usage String Best Practices](#usage-string-best-practices)
148. [Testing Permissions](#testing-permissions)
15 
16---
17 
18## Permission Request Philosophy
19 
20**Core principle:** Request permissions only when needed, explain why, and respect "no."
21 
22Users 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 
30Request 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 
37Before 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```swift
69// Camera
70AVCaptureDevice.requestAccess(for: .video) { granted in
71 // Handle response
72}
73 
74// Photos
75PHPhotoLibrary.requestAuthorization(for: .readWrite) { status in
76 // Handle status
77}
78 
79// Location
80locationManager.requestWhenInUseAuthorization()
81// or
82locationManager.requestAlwaysAuthorization()
83 
84// Notifications
85UNUserNotificationCenter.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```swift
126// PHPicker - no permission required
127var config = PHPickerConfiguration()
128config.selectionLimit = 1
129config.filter = .images
130 
131let 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```swift
153// Request when in use first
154locationManager.requestWhenInUseAuthorization()
155 
156// Later, if needed, escalate to always
157// (triggers new system prompt explaining upgrade)
158locationManager.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```swift
177// Quietly delivered to Notification Center
178// User can choose to keep or turn off
179UNUserNotificationCenter.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```swift
224ATTrackingManager.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 
247Always 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 
259Once denied, system won't show prompt again. Guide users to Settings.
260 
261```swift
262func 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 
291If user taps "Maybe Later" on pre-permission screen, don't immediately ask again.
292 
293```swift
294// Track dismissal
295UserDefaults.standard.set(Date(), forKey: "camera_prompt_dismissed")
296 
297// Wait before showing again
298func 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 
315Show 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 
330Explain 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 
355Provide 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 
372Your 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```bash
424# Reset all permissions for specific app
425xcrun simctl privacy booted reset all com.yourapp.bundleid
426 
427# Reset specific permission
428xcrun simctl privacy booted reset camera com.yourapp.bundleid
429```
430 
431### Test All States
432 
433For each permission:
4341. Never requested (first launch)
4352. Authorized
4363. Denied
4374. Restricted (parental controls)
4385. Limited (Photos)
4396. Provisional (Notifications)
440 
441### Automated Testing
442 
443```swift
444func 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