On this page 10 sections
- Start with the least access that still works
- PHPicker versus the full library
- Limited library access in PhotoKit
- Permission strings that tell the truth
- Reading, writing and deleting safely
- iCloud, iCloud+ and assets that are not downloaded yet
- What App Review looks for
- A simple architecture for a photo feature
- Key takeaways
- Frequently asked questions
The privacy manifests guide covers declarations and required-reason APIs. This article is the Photos-specific practice: picker versus PhotoKit, limited access, truthful strings, reading and writing, and the iCloud cases that make a “simple” image feature fail on a real phone.
Start with the least access that still works
Map the feature to an access level before you import PhotoKit:
- The user picks one or more existing photos to send, edit or attach. You do not need to browse their library yourself. Use
PHPickerViewControlleror SwiftUI’sPhotosPicker. No library permission, no purpose string for reading, no limited-access banner. - The app creates an image and the user wants it in Photos. Request
PHAccessLevel.addOnly, withNSPhotoLibraryAddUsageDescription. You can write; you cannot enumerate the library. - The app is a library client — a custom picker, a backup tool, a slideshow of a specific album you must keep in sync. Request read/write (or read, if you will not write), handle limited library, and be prepared to work with a subset of assets. This is the expensive option, in review and in engineering.
Do not request read/write because it makes the demo easier, or because you might add an album feature later. You can upgrade access later; you cannot easily un-ask.
If the feature is not central, consider whether it belongs in the MVP at all. A first version that shares an image with UIActivityViewController or saves via the add-only path is often enough.
PHPicker versus the full library
PHPicker is the default I reach for. The system presents the library, the user selects, and you receive item providers. You never see the photos they did not pick, and you do not hold a PHAsset unless you ask for it in the configuration.
import PhotosUI
import SwiftUI
struct AvatarPicker: View {
@State private var selection: PhotosPickerItem?
var body: some View {
PhotosPicker("Choose a photo", selection: $selection, matching: .images)
.onChange(of: selection) {
Task { await load(item: selection) }
}
}
func load(item: PhotosPickerItem?) async {
guard let item, let data = try? await item.loadTransferable(type: Data.self) else {
return
}
// Decode, downsize, and strip metadata before you upload.
_ = data
}
}Configure the picker to match the job: images only, a selection limit, maybe live photos if you truly support them. Do not request every representation “in case”.
Use a custom PhotoKit grid only when the picker cannot do the job: you need a particular album layout, a selection model the system sheet cannot express, or a persistent relationship to assets the user has already granted. If you go that way, limited access is the common outcome, not a corner case, and your UI must handle an empty or partial library without looking broken.
UIImagePickerController still exists; for new work, prefer PHPicker. It is faster to configure, it supports limited access correctly, and it is the path Apple documents for a privacy-preserving choice.
Limited library access in PhotoKit
Since iOS 14, a user can grant access only to selected photos. Your app’s PHAsset fetches then see that subset, not the whole library. They can change the subset later, from Settings or from a limited-library picker you present with PHPhotoLibrary.shared().presentLimitedLibraryPicker(from:).
Handle all of these states, not only “authorized” and “denied”:
import Photos
func prepareLibraryAccess() async -> PHAuthorizationStatus {
let status = PHPhotoLibrary.authorizationStatus(for: .readWrite)
switch status {
case .notDetermined:
return await PHPhotoLibrary.requestAuthorization(for: .readWrite)
case .limited, .authorized, .denied, .restricted:
return status
@unknown default:
return status
}
}When the status is .limited:
- Fetch as usual; you will only see the chosen assets. Design empty states (“No selected photos yet”) and a control that opens the limited-library picker so the user can add more.
- Register for
PHPhotoLibraryChangeObserverso a change in the selection, or a deleted photo, updates your UI. - Do not tell the user the library is “empty” as if they have no photos. That is how you train them to tap “Allow All” out of confusion, which Review will not thank you for if your purpose was thin.
- Do not try to infer or enumerate photos outside the grant. That is the point of the restriction.
Listen for changes to authorisation as well as to the library. A user can downgrade you in Settings while the app is open.
Permission strings that tell the truth
NSPhotoLibraryUsageDescription (read/write) and NSPhotoLibraryAddUsageDescription (add only) are user-facing sentences, and they are also Review-facing. They must describe the actual feature, in language a person can understand.
Good, if they are true:
- “We save photos you take in the app to your library when you tap Save.”
- “You can choose photos to attach to a report. We only access the photos you select.”
Poor, and regularly rejected or later complained about:
- “We need access to your photos to improve your experience.”
- “This lets us provide better services.”
- A string that talks about uploading or sharing when the button on screen is only “Set avatar”.
- A read/write string on an app that only saves, because you copied a plist from another target.
If you use the picker and never request library access, you may not need a read usage string at all. Do not add one “for later”. If you request add-only, ship only the add string until you truly read.
The prompt will present your sentence with a system heading and, for read/write, the limited-versus-full choice. Your UI should not pre-empt that choice with “tap Allow All or the app will not work” unless full access is genuinely required — and if it is, you should be able to say why in the purpose string and in a Review note.
Keep the same story in your privacy policy, your privacy manifest, and the App Store privacy labels, as the broader privacy guide argues. Inconsistency is easier to spot here than in almost any other permission.
Reading, writing and deleting safely
When you do use PhotoKit, be specific about what you request from PHImageManager. Ask for the size you will display or upload, not an implicit full-resolution image for a 80-point thumbnail. A resizer or a compressor on this site can help you think about the target size; in the app, do the same work with PHImageRequestOptions and a target size, or with ImageIO, so a 12-megapixel asset does not go through memory as a bitmap twice.
import Photos
import UIKit
func requestThumbnail(for asset: PHAsset, size: CGSize) async -> UIImage? {
await withCheckedContinuation { continuation in
let options = PHImageRequestOptions()
options.deliveryMode = .opportunistic
options.resizeMode = .fast
options.isNetworkAccessAllowed = true
PHImageManager.default().requestImage(
for: asset,
targetSize: size,
contentMode: .aspectFill,
options: options
) { image, info in
let degraded = (info?[PHImageResultIsDegradedKey] as? Bool) ?? false
if !degraded {
continuation.resume(returning: image)
}
}
}
}For writing, add-only is the usual path:
func saveToLibrary(_ image: UIImage) async throws {
let status = await PHPhotoLibrary.requestAuthorization(for: .addOnly)
guard status == .authorized else {
throw PhotoSaveError.notAllowed
}
try await PHPhotoLibrary.shared().performChanges {
PHAssetChangeRequest.creationRequestForAsset(from: image)
}
}Deleting or editing an asset in the library is a bigger deal than saving. Use it only when the product is clearly a library editor, confirm with the user, and handle a failed change. Do not delete originals as a side effect of a “remove from this app” button unless that is exactly what you promised.
Before you upload, think about metadata. Photos often carry location, device and timestamps in EXIF. If your server or a share should not receive a location, strip it. The EXIF viewer and stripper is a way to inspect a sample file; in the app, use ImageIO or a checked library to write a clean image. Do not assume UIImage.jpegData has already removed everything you care about — check.
iCloud, iCloud+ and assets that are not downloaded yet
Many libraries keep originals in iCloud. A PHAsset can exist, and a thumbnail can exist, while the full data is still in the cloud. If isNetworkAccessAllowed is false, the request can fail or return only a degraded image. If it is true, the request may take time, use cellular data, and still fail on a plane.
Product rules that save pain:
- Show the degraded image first, and replace it, rather than blocking the grid on full-size downloads.
- Let the user start a full-quality export deliberately, with a progress indicator, when original quality matters.
- Handle failure as a normal state: “This photo is not downloaded” with a retry, not a generic “Something went wrong”.
- Do not request dozens of originals at once in a backup feature without a queue, a budget, and a pause on cellular if the user would expect Wi-Fi only.
iCloud Shared Albums and Hidden / Recently Deleted collections have their own rules about what an app can see. Do not assume your fetch covers them. Test with a library that has iCloud Photos turned on, a photo that is not stored locally, and a user who has granted only limited access. A simulator with five stock images will not expose this.
What App Review looks for
Review sees the prompt, the purpose string, and what the app does next. Common reasons a photo permission is rejected, in my experience and in the guidelines:
- The purpose string is vague, or it does not match the screen that triggered the prompt.
- The app requests full library access when a picker would do, especially if the first prompt is on launch.
- The app refuses to function unless the user grants Access All Photos, without a limited-access path that already satisfies the feature.
- A usage string is present for a permission the binary does not need, or the privacy labels disagree with the behaviour.
- The app appears to upload photos or metadata the user did not expect, including location in EXIF.
Put a short Review note if the photo feature is easy to miss: which button opens the picker, whether a test account has content, and whether you only need add-only. Do not ask Review to “just allow access” as a substitute for a picker-first design.
A simple architecture for a photo feature
A structure that has stayed maintainable:
- A single photo service that owns permission status, picker configuration, save, and (if you must) PhotoKit fetches. Views do not call
PHPhotoLibrarydirectly. - A picker-first public API, plus a narrow library API that is unused in builds that do not need it.
- Explicit models for “user-selected image data” versus “a
PHAssetidentifier we may see again”. If you only needed the bytes once, do not store a local identifier and pretend you still have access after the user narrowed the grant. - A processing pipeline that resizes, compresses and strips metadata before upload or long-term storage. Keep the original only if the product is an editor and the user still has the asset.
- Previews and tests with fixture images, not with the live library. You can still run one device pass against iCloud and limited access before you ship.
That service is also the place to log, carefully, that a request failed because the asset was in the cloud, or because access was limited — not the place to log identifiers that could identify a person’s private photos.
Key takeaways
- Use the system picker when the user is choosing a photo; it needs no library permission.
- Use add-only access when you only save. Request read/write only for a genuine library feature, and handle limited access as the normal grant.
- Write purpose strings that name the feature, and keep them consistent with the UI, the privacy policy and the manifest.
- Request the image size you need, strip metadata before a share or upload if location should not travel, and confirm deletes.
- Test with iCloud originals that are not on the device, and with a limited selection, not only with a simulator library.
- Review looks for a matching story and a picker-first design. Full access on launch is a red flag.
If you want a second pair of eyes on a photo permission or a picker flow, tell me about it.
Frequently asked questions
What is limited photo library access on iOS?
It is a grant, offered when you ask for library access, that lets the user pick which photos your app can see. Fetches return only that subset. The user can change the selection later, and your UI must cope with a small or empty grant without implying that the device has no photos, or that only “Allow All” will work.
Should you use PHPicker or request full PhotoKit access?
Use PHPicker (or SwiftUI PhotosPicker) when the user is choosing photos for a task. It does not require a library permission. Request PhotoKit library access only when you need a custom library UI or a lasting relationship with assets. Full access is rarer than limited access, and rarer still than the picker.
How do you handle the iOS photo permission prompt?
Ask at the moment of the feature, with a purpose string that describes that feature, and only at the access level you need. Prefer a flow that does not prompt at all (the system picker). If you must prompt, support limited access, and do not refuse to work just because the user did not choose Access All Photos.
Why did App Review reject a photo permission?
Usually because the purpose string was vague or untrue, the app asked for full library access when a picker would do, the prompt appeared too early, or the privacy labels did not match the behaviour. Fix the access level and the wording so they describe the same feature, then resubmit with a note that points at the button.
How do you write an image into the photo library in Swift?
Request add-only access, then PHPhotoLibrary.shared().performChanges with PHAssetChangeRequest.creationRequestForAsset(from:), or the URL equivalent. Handle a denied status without crashing. You do not need read access to save, and you should not request it only to write.
Written by Lee Johnson. Published 10 November 2026.