Accessing the photo library in SwiftUI
02 Jan 2021
SwiftUI doesn’t yet have an easy way to access the camera or photo library. hackingwithswift.com already has a great tutorial on how to access the camera from SwiftUI.
This article will cover how to access the user’s photo library using PHPicker
, a new privacy-first API introduced in iOS 14 which may eventually replace UIImagePickerController
. We’ll build a simple SwiftUI app that accesses the user’s photo library and displays the selected photo on screen.
View the code for this tutorial on GitHub
Building the bridge
As with all UIKit views accessed in SwiftUI, we must start by building a bridge. I’ve added comments inline with the code to explain what’s happening at each stage.
import SwiftUI
import PhotosUI
struct PhotoPicker: UIViewControllerRepresentable {
typealias UIViewControllerType = PHPickerViewController
// Can be .images, .livePhotos or .videos
let filter: PHPickerFilter
// How many photos can be selected. 0 means no limit.
var limit: Int = 0
func makeUIViewController(context: Context) -> PHPickerViewController {
// Create the picker configuration using the properties passed in above.
var configuration = PHPickerConfiguration()
configuration.filter = filter
configuration.selectionLimit = limit
// Create the view controller.
let controller = PHPickerViewController(configuration: configuration)
// Link it to the Coordinator created below.
controller.delegate = context.coordinator
return controller
}
// This method is blank because it will never be updated.
func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
class Coordinator: PHPickerViewControllerDelegate {
// The coordinator needs a reference to the thing it's linked to.
private let parent: PhotoPicker
init(_ parent: PhotoPicker) {
self.parent = parent
}
// Called when the user finishes picking a photo.
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
// Dismiss the picker.
picker.dismiss(animated: true)
}
}
}
Calling the bridge
From our regular SwiftUI view code, we now need to call the code we just wrote.
import SwiftUI
struct ContentView: View {
@State private var showPhotoPicker = false
@State private var selectedImage: UIImage? = nil
var body: some View {
VStack {
Button(action: { showPhotoPicker = true }) {
Label("Choose photo", systemImage: "photo.fill")
}
.fullScreenCover(isPresented: $showPhotoPicker) {
// Create the picker. We only want to allow the user to select a single image.
// We ignore the safe area so that the picker takes up the entire screen when open.
PhotoPicker(filter: .images, limit: 1)
.edgesIgnoringSafeArea(.all)
}
// Display the image if one has been selected.
if let image = selectedImage {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: 200, maxHeight: 200)
}
}
}
}
Run the code. You should notice two things:
- You didn’t have to get permission to access the photo library. The beauty of
PHPicker
is that it works outside of the usual permission system. It runs separately from our app and only grants access to the photos the user explicitly chooses to grant access to. - When you select a photo, the picker closes but nothing else happens. This is because we haven’t hooked it up properly yet. Let’s do that now.
Adding event handling
Go back to the bridge we built earlier. We’re going to add a new member to PhotoPicker
, a function that will be called when the user selects a photo.
struct PhotoPicker: UIViewControllerRepresentable {
typealias UIViewControllerType = PHPickerViewController
let filter: PHPickerFilter
var limit: Int = 0
let onComplete: ([PHPickerResult]) -> Void
...
Then, in the Coordinator
class, add this line to the picker(_, didFinishPicking)
function:
// Call the completion handler.
parent.onComplete(results)
Convert the results to images
You’ll notice that the picker returns its results as [PHPickerResult]
, not [UIImage]
. Therefore, we must first convert the results to images before we can use them.
There are two approaches to doing this:
- Convert the results automatically inside
PhotoPicker
before theonComplete
function is called. - Add a function to convert
[PHPickerResult]
to[UIImage]
that can be called from the view.
In this article I’m following option 2 because I prefer to follow a composition over configuration approach in my code. This approach also leaves room for adding more functions in the future, for example one to return videos instead of images if the picker is being used to select videos.
Add a new function to PhotoPicker
:
static func convertToUIImageArray(fromResults results: [PHPickerResult], onComplete: @escaping ([UIImage]?, Error?) -> Void) {
// Will be used to store the images that get created from results.
var images = [UIImage]()
let dispatchGroup = DispatchGroup()
for result in results {
dispatchGroup.enter()
let itemProvider = result.itemProvider
if itemProvider.canLoadObject(ofClass: UIImage.self) {
itemProvider.loadObject(ofClass: UIImage.self) { (imageOrNil, errorOrNil) in
if let error = errorOrNil {
onComplete(nil, error)
}
if let image = imageOrNil as? UIImage {
images.append(image)
}
dispatchGroup.leave()
}
}
}
dispatchGroup.notify(queue: .main) {
onComplete(images, nil)
}
}
Then in our SwiftUI view, add the onComplete
handler to where we call PhotoPicker
, along with a call to the converter function we just wrote:
...
.fullScreenCover(isPresented: $showPhotoPicker) {
PhotoPicker(filter: .images, limit: 1) { results in
PhotoPicker.convertToUIImageArray(fromResults: results) { imagesOrNil, errorOrNil in
if let error = errorOrNil {
print(error)
}
if let images = imagesOrNil {
if let first = images.first {
// Update our state with the image.
selectedImage = first
}
}
}
}
.edgesIgnoringSafeArea(.all)
}
Run the code again and it’ll now work. When you select an image, it’s passed to our view, converted to a UIImage
and then displayed on screen.
The yellow UI elements are a bug in this version of the iOS simulator.
Conclusion
Today we learned how to bridge PHPicker
from UIKit to SwiftUI. If you have any questions or comments, feel free to reach out via Twitter or email. Links to both are in the footer of this page.
Thanks for reading.
To get in touch, email me or find me on Mastodon or Twitter.
If you liked this post, you'll love my iOS apps. Check them out below.