Understanding the iOS Photos Framework Architecture

The Photos framework (also referred to as PhotoKit) is Apple's primary API for interacting with the user's photo and video library on iOS, iPadOS, and macOS. It replaces the older Assets Library framework and provides a modern, object-oriented interface that handles large media collections efficiently while respecting user privacy. The framework is built around a few core classes that represent media assets, collections, and the operations you can perform on them.

At the heart of the framework is the PHPhotoLibrary singleton, which represents the user's entire photo library. All changes to the library — adding, deleting, or editing assets — must be performed through its performChanges methods. The library also emits change notifications that allow your app to stay synchronized when the user modifies media from other apps or from iCloud.

Media items are modeled by PHAsset, which represents a single photo, video, or Live Photo. Each asset has immutable metadata such as creation date, location, type, and whether it is a favorite. Collections of assets are represented by PHAssetCollection (for albums, Moments, or user-created folders) and PHCollectionList (for folders that contain other collections).

The framework also includes two key services for fetching and displaying media: PHFetchResult (the result of a fetch request) and PHImageManager (for loading images and video thumbnails at specified sizes while optimizing memory usage and performance).

Requesting Access to the Photo Library

Before your app can access any photo library content, you must request authorization from the user. Starting with iOS 14, Apple introduced a more granular privacy model that allows users to grant either full access or limited access to selected photos. This change also introduced the PHAuthorizationStatusLimited status, which requires special handling to avoid a poor user experience.

The authorization request is made using PHPhotoLibrary.requestAuthorization(for:), passing the PHAccessLevel of either .readWrite or .addOnly. For most media management apps, you will need read-write access to display, add, or delete content. The completion handler returns an authorization status; you should respond accordingly by either showing the library content or guiding the user to Settings to change permissions.

To request full access, your app’s Info.plist must include the NSPhotoLibraryUsageDescription key (required for any access) and optionally the NSPhotoLibraryAddUsageDescription key if you only need to add media without reading. For limited access, consider prompting the user to expand their selection or explaining why full access is beneficial.

Handling Limited Photo Library Access

When the user grants limited access, your fetch requests will return only the assets they selected. You can detect this state by checking PHPhotoLibrary.authorizationStatus(for: .readWrite) and comparing it to .limited. In limited mode, you should present a clear interface that shows only the available assets and, if appropriate, provide a button to open the system photo library picker (using PHPickerViewController) so the user can modify their selection.

Fetching Media Assets

The Photos framework uses a generic fetch pattern that returns PHFetchResult objects. These fetch results are ordered and support efficient enumeration and random access. You can fetch all assets of a certain type, filter by creation date, location, or media subtypes, and even sort the results.

Basic fetch examples:

  • Fetch all images: PHAsset.fetchAssets(with: .image, options: options)
  • Fetch all videos: PHAsset.fetchAssets(with: .video, options: options)
  • Fetch all assets in a specific album: PHAsset.fetchAssets(in: album, options: options)

The PHFetchOptions class lets you apply predicates, sort descriptors, and limit the fetch set. You can also fetch only assets that are marked as favorites or that contain specific metadata. For performance, always specify as precise a predicate as possible. Avoid fetching all assets and then filtering in memory; instead, use NSPredicate to let the framework do the filtering internally.

Displaying Images and Videos

Once you have a PHAsset, you need to load its image or video frames for display. The PHImageManager class (singleton accessed via PHImageManager.default()) handles this asynchronously with built-in caching and downsampling. You request images by specifying a target size and content mode.

For example, to load a thumbnail for a table view cell:

PHImageManager.default().requestImage(
    for: asset,
    targetSize: CGSize(width: 100, height: 100),
    contentMode: .aspectFill,
    options: nil) { image, info in
        cell.imageView.image = image
}

For larger images, such as full-screen photos, you should use PHImageRequestOptionsDeliveryMode with .highQualityFormat and possibly request the image synchronously on a background thread. Always handle the download progress and error cases from the info dictionary.

Caching Images for Scrolling Performance

For collection views or table views that display many thumbnails, use PHCachingImageManager (a subclass of PHImageManager) to prefetch images as the user scrolls. This class allows you to start and stop caching for sets of assets, dramatically improving scrolling smoothness. After setting up the caching manager, call startCachingImages(for:targetSize:contentMode:options:) with the assets that are about to become visible.

Adding New Media to the Library

Adding photos or videos from your app into the user’s library requires calling PHPhotoLibrary.shared().performChanges with a change block. Inside the block, you create a PHAssetChangeRequest from your image data or file URL. The framework will handle the atomic save and notify any observers.

Example to add a UIImage:

PHPhotoLibrary.shared().performChanges {
    let creationRequest = PHAssetChangeRequest.creationRequestForAsset(from: image)
    creationRequest.creationDate = Date()
} completionHandler: { success, error in
    if !success { print("Error: \(error?.localizedDescription ?? "")") }
}

You can also specify the asset’s location, favorite status, and more by customizing the PHAssetChangeRequest object before the library saves. For video files, use creationRequestForAssetFromVideo(atFileURL:).

Deleting Media Assets

Deletion follows the same change pattern. Inside a performChanges block, you create a PHAssetChangeRequest and call deleteAssets with an array of assets. The deletion is permanent — the user cannot recover deleted photos from your app. Therefore, always ask for confirmation and consider implementing a “recently deleted” feature if needed.

Example deletion:

PHPhotoLibrary.shared().performChanges {
    PHAssetChangeRequest.deleteAssets(assets as NSFastEnumeration)
} completionHandler: { success, error in
    // handle result
}

Editing Media Assets

The Photos framework supports editing both photos and videos. For photos, you can apply adjustments (such as filters, cropping, or exposure changes) and save the result back to the library in a nondestructive manner. The original asset is preserved, and the latest adjustment is shown in the Photos app.

To edit a PHAsset, follow these steps:

  1. Request a PHContentEditingInput using PHImageManager.requestContentEditingInput(with:options:).
  2. Extract the full‑size image data or video composition.
  3. Apply your edits and create a PHAdjustmentData object that describes the edit (you can include your app’s identifier and serialized settings).
  4. Use PHPhotoLibrary.performChanges with a PHAssetChangeRequest to save the edited output and the adjustment data.

When the user later views the edited asset in Photos, the system will use your adjustment data to reconstruct the edit if your app is installed. This allows a seamless editing experience across apps.

Observing Changes in the Photo Library

To keep your app’s UI in sync with the photo library, adopt the PHPhotoLibraryChangeObserver protocol. Register your observer with PHPhotoLibrary.shared().register(self) and implement photoLibraryDidChange(_:). The change instance provides details about inserted, changed, or removed assets and collections. You can use the change details to update your fetch results and collection views without re-fetching everything.

If you use a PHFetchResult and want to update it efficiently, call changeDetails(for:) on the change instance to get a PHFetchResultChangeDetails object that contains the before‑and‑after fetch results and the required index changes.

Performance Best Practices

Working with a large photo library (thousands of assets) requires careful performance tuning. The Photos framework is optimized for large collections, but you can still hit performance bottlenecks if you misuse the APIs.

  • Always use specific PHFetchOptions predicates and sort descriptors to minimise the number of assets loaded.
  • For scrolling grids, use PHCachingImageManager and request thumbnails at the exact cell size (not larger than needed).
  • Avoid synchronous image requests on the main thread — they block the UI. Use the asynchronous requestImage method instead.
  • When enumerating a fetch result, use enumerateObjects rather than index-based access in a loop, as it is more memory efficient.
  • On iOS 15 and later, consider using the new PHImageRequestOptions property isSynchronous with a timeout for better control.

Privacy and Security Considerations

User privacy is paramount when handling photo library data. Beyond requesting proper authorization, Apple requires that apps using the Photos framework provide a privacy manifest file (required as of iOS 17 and later). This file declares the reason codes for using the photo library API and must match your actual usage.

When the user grants limited access, your app should never attempt to “trick” them into selecting more photos than necessary. Avoid observing the photo library change notifications for the purpose of tracking when the user changes their selection in Settings — this is considered a privacy violation and can lead to rejection.

For apps that only need to import media without viewing the existing library, use PHPickerViewController (from the PhotosUI framework) instead. The picker runs in a separate process and does not require any photo library access permission, providing enhanced privacy. You can configure the picker to allow multiple selections and specify media types.

Integrating with Directus (Headless CMS)

While this article focuses on the iOS Photos framework, many apps today need to upload user-selected media to a backend like Directus. The combination works naturally: after the user picks media through your app’s UI (built with PHImageManager or PHPicker), you upload the image or video data to Directus using its REST or GraphQL API. Directus stores the media as assets and returns a unique ID you can associate with other content. You can also use Directus’s built-in image transformations to generate thumbnails or resize images for different device sizes.

For best results, compress the image data on the device before uploading — use UIImageJPEGRepresentation with a quality factor of 0.8 or convert to HEIF if the device supports it. For videos, consider transcribing to a lower resolution or trimming to a manageable length before upload. Directus can also handle video transcoding server-side if you prefer to upload the original.

Conclusion

The iOS Photos framework provides a rich, performant set of tools for adding media management to your app. From fetching and displaying assets to editing and uploading, the framework gives you full control while protecting user privacy. By following the best practices outlined above — requesting appropriate authorization, using caching for smooth scrolling, and leveraging change notifications to keep your UI up‑to‑date — you can build a polished, user‑trusting media experience. For server‑side media storage, combining the Photos framework with a headless CMS like Directus creates a seamless pipeline from the user’s device to the cloud.

For more information, see Apple’s official PhotoKit documentation and the PHPhotoLibraryChangeObserver protocol reference.