- Published on
- 7 min read
> Accessing the Camera Roll in SwiftUI
Working with the photo library in SwiftUI has gotten much easier since iOS 16 introduced PhotosPicker. Before diving into code, you'll need to configure your app's Info.plist with privacy descriptions explaining why you need access to photos.
Setting Up Info.plist
Your app will crash the first time it tries to access photos without the proper Info.plist entries. Add these keys based on what your app needs to do:
Reading Photos
Every app that reads photos needs NSPhotoLibraryUsageDescription:
<key>NSPhotoLibraryUsageDescription</key>
<string>We need access to your photos to let you select images for your profile.</string>
Saving Photos
If you're saving images to the library, add NSPhotoLibraryAddUsageDescription:
<key>NSPhotoLibraryAddUsageDescription</key>
<string>We need permission to save images to your photo library.</string>
Starting in iOS 14, you only need the "add" key if you're exclusively saving photos. If you need both read and write access, include both.
Adding Keys in Xcode
The easiest way is through Xcode's Info tab:
- Select your target
- Go to the Info tab
- Click "+" to add a key
- Start typing "Privacy - Photo Library" and Xcode will autocomplete
- Enter a clear description users will actually understand
You can also edit Info.plist directly as XML if you prefer. Just right-click the file and choose "Open As" then "Source Code".
One important thing: your app will crash if you forget to add these privacy keys. Always test on a real device to catch this early. The simulator sometimes lets you get away with it.
iOS 16+: PhotosPicker
iOS 16 brought PhotosPicker to SwiftUI, and it's the best way to let users pick photos. The big win here is that users can grant access to specific photos without opening up their entire library to your app. This privacy-first design means you should use PhotosPicker whenever you can.
import SwiftUI
import PhotosUI
struct PhotoPickerView: View {
@State private var selectedItem: PhotosPickerItem?
@State private var selectedImage: Image?
var body: some View {
VStack {
PhotosPicker(selection: $selectedItem, matching: .images) {
Label("Select Photo", systemImage: "photo")
}
if let selectedImage {
selectedImage
.resizable()
.scaledToFit()
.frame(maxWidth: 300)
}
}
.onChange(of: selectedItem) { _, newItem in
Task {
if let data = try? await newItem?.loadTransferable(type: Data.self),
let uiImage = UIImage(data: data) {
selectedImage = Image(uiImage: uiImage)
}
}
}
}
}
Selecting Multiple Photos
Want users to pick several photos? Add maxSelectionCount:
struct MultiplePhotoPickerView: View {
@State private var selectedItems: [PhotosPickerItem] = []
@State private var selectedImages: [Image] = []
var body: some View {
VStack {
PhotosPicker(
selection: $selectedItems,
maxSelectionCount: 5,
matching: .images
) {
Label("Select Photos (Max 5)", systemImage: "photo.on.rectangle")
}
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 100))]) {
ForEach(selectedImages.indices, id: \.self) { index in
selectedImages[index]
.resizable()
.scaledToFill()
.frame(width: 100, height: 100)
.clipped()
}
}
}
}
.onChange(of: selectedItems) { _, newItems in
Task {
selectedImages = []
for item in newItems {
if let data = try? await item.loadTransferable(type: Data.self),
let uiImage = UIImage(data: data) {
selectedImages.append(Image(uiImage: uiImage))
}
}
}
}
}
}
Filtering What Users Can Pick
You can restrict the picker to specific media types:
// Only photos, no videos
PhotosPicker(selection: $selectedItem, matching: .images)
// Only videos
PhotosPicker(selection: $selectedItem, matching: .videos)
// Everything except videos
PhotosPicker(selection: $selectedItem, matching: .any(of: [.images, .not(.videos)]))
// Only Live Photos
PhotosPicker(selection: $selectedItem, matching: .livePhotos)
iOS 14-15: UIImagePickerController
Before PhotosPicker existed, we had to wrap UIKit's UIImagePickerController. It still works fine if you need to support iOS 14 or 15:
import SwiftUI
import UIKit
struct ImagePicker: UIViewControllerRepresentable {
@Binding var selectedImage: UIImage?
@Environment(\.dismiss) var dismiss
func makeUIViewController(context: Context) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.delegate = context.coordinator
picker.sourceType = .photoLibrary
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
let parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
func imagePickerController(
_ picker: UIImagePickerController,
didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]
) {
if let image = info[.originalImage] as? UIImage {
parent.selectedImage = image
}
parent.dismiss()
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
parent.dismiss()
}
}
}
Using it in a view:
struct LegacyPhotoPickerView: View {
@State private var selectedImage: UIImage?
@State private var showingPicker = false
var body: some View {
VStack {
Button("Select Photo") {
showingPicker = true
}
if let selectedImage {
Image(uiImage: selectedImage)
.resizable()
.scaledToFit()
.frame(maxWidth: 300)
}
}
.sheet(isPresented: $showingPicker) {
ImagePicker(selectedImage: $selectedImage)
}
}
}
Saving Photos to the Library
To save an image, use UIImageWriteToSavedPhotosAlbum:
import SwiftUI
import UIKit
struct SavePhotoView: View {
@State private var showingSaveConfirmation = false
var body: some View {
Button("Save Photo") {
saveImageToLibrary()
}
.alert("Saved", isPresented: $showingSaveConfirmation) {
Button("OK", role: .cancel) { }
} message: {
Text("Photo saved to your library")
}
}
func saveImageToLibrary() {
// Create a simple colored square as an example
let renderer = UIGraphicsImageRenderer(size: CGSize(width: 512, height: 512))
let image = renderer.image { ctx in
UIColor.systemBlue.setFill()
ctx.fill(CGRect(origin: .zero, size: CGSize(width: 512, height: 512)))
}
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
showingSaveConfirmation = true
}
}
If you need to know whether the save succeeded, you'll need a bit more code:
func saveImageWithCompletion(_ image: UIImage) {
class ImageSaver: NSObject {
var successHandler: (() -> Void)?
var errorHandler: ((Error) -> Void)?
func writeToPhotoAlbum(image: UIImage) {
UIImageWriteToSavedPhotosAlbum(image, self, #selector(saveCompleted), nil)
}
@objc func saveCompleted(
_ image: UIImage,
didFinishSavingWithError error: Error?,
contextInfo: UnsafeRawPointer
) {
if let error = error {
errorHandler?(error)
} else {
successHandler?()
}
}
}
let saver = ImageSaver()
saver.successHandler = {
print("Image saved successfully")
}
saver.errorHandler = { error in
print("Save error: \(error.localizedDescription)")
}
saver.writeToPhotoAlbum(image: image)
}
Understanding Limited Photo Access
iOS 14 changed the game by introducing limited photo library access. Now when users first grant permission, they can choose to give your app access to only specific photos instead of their entire library.
Here's a key point about timing: don't ask for photo access when your app launches. Wait until users actually try to do something that needs photos. It makes the permission request feel less intrusive and more contextual.
PhotosPicker handles this automatically. It just works. But if you're using PhotoKit's PHPhotoLibrary APIs directly, you'll need to request permission:
import Photos
func requestPhotoLibraryPermission() {
PHPhotoLibrary.requestAuthorization(for: .readWrite) { status in
switch status {
case .authorized:
print("Full access granted")
case .limited:
print("Limited access granted")
case .denied, .restricted:
print("Access denied")
case .notDetermined:
print("Not determined")
@unknown default:
break
}
}
}
You can check the current status before requesting:
let status = PHPhotoLibrary.authorizationStatus(for: .readWrite)
if status == .notDetermined {
requestPhotoLibraryPermission()
}
Handling Permission Denials
If users deny photo access, you'll want to direct them to Settings to change it.
One thing that's helpful during development: if you're testing permission flows and need to reset them, go to Settings → General → Transfer or Reset iPhone → Reset Location & Privacy. This clears all permission decisions so you can test the first-run experience again.
struct PermissionDeniedView: View {
var body: some View {
VStack(spacing: 20) {
Image(systemName: "photo.badge.exclamationmark")
.font(.system(size: 60))
.foregroundColor(.red)
Text("Photo Access Denied")
.font(.title)
Text("Please enable photo access in Settings to select images.")
.multilineTextAlignment(.center)
.foregroundColor(.secondary)
Button("Open Settings") {
if let settingsURL = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(settingsURL)
}
}
.buttonStyle(.borderedProminent)
}
.padding()
}
}
Quick Reference by iOS Version
iOS 16 and later: Use PhotosPicker. It's native SwiftUI, handles permissions automatically, and respects limited library access without any extra work from you.
iOS 14-15: Wrap UIImagePickerController. It requests permission automatically when users tap the picker.
iOS 11-13: Same as 14-15, but you'll need both NSPhotoLibraryUsageDescription and NSPhotoLibraryAddUsageDescription if you're both reading and writing.
// Continue_Learning
Detecting When a Screenshot is Taken in SwiftUI
Learn how to detect when users take screenshots in your SwiftUI app by observing UIApplication notifications.
Preventing Screenshot Capture in SwiftUI Views
Learn how to prevent users from taking screenshots of sensitive content in your SwiftUI app using field-level security and UIKit bridging for financial, medical, or private data protection.
How to display a GIF from a URL in SwiftUI
Learn how to use a WKWebView as a quick way to load a GIF from a URL and display it in a SwiftUI view.
// Stay Updated
Get notified when I publish new tutorials on Swift, SwiftUI, and iOS development. No spam, unsubscribe anytime.