Start a project

Article · iOS · PhotoKit · Privacy · 11 min read

Privacy-safe photo access with PhotoKit

The privacy-safe way to work with photos on iOS is to start with the least access that still does the job. If the user is choosing a picture, use the system picker, which does not require a photo library permission. If you only need to save a picture, ask for add-only access. Use PhotoKit against the library, including limited library access, only when you truly need a custom grid or to keep a relationship with specific assets. Full library access is a last resort, and App Review will look at whether your purpose string and your UI tell the same story.

On this page 10 sections
  1. Start with the least access that still works
  2. PHPicker versus the full library
  3. Limited library access in PhotoKit
  4. Permission strings that tell the truth
  5. Reading, writing and deleting safely
  6. iCloud, iCloud+ and assets that are not downloaded yet
  7. What App Review looks for
  8. A simple architecture for a photo feature
  9. Key takeaways
  10. 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:

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:

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:

Poor, and regularly rejected or later complained about:

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:

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:

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:

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

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.

All articles · Swift A–Z · Apps

Questions, or an app in mind?

Let's bring your ideas to life. Whether you're envisioning a game-changing Apple Watch app or a transformative iOS experience, I'm here to make it happen.

Start a project