Back in 2020 Apple presented a new photos picker API to developers known as the PHPicker API. PHPicker lets users get access to their photos in a safe manner through third-party apps, as the picker runs in a different process and does not require full access to Photos library. That API has to offer some great features and capabilities, however its disadvantage lies to the fact that it was not designed to work natively in SwiftUI. It’s a perfect fit for UIKit, but a UIViewControllerRepresentable type has to be implemented mandatorily as the middle man between PHPicker API and SwiftUI.
That changed in WWDC 2022 though, when Apple finally introduced PhotosPicker; a SwiftUI native view that provides the full range of functionalities that PHPicker API does -its counterpart actually- but lifting all the unavoidable complexities and hassle required until that point.
PhotosPicker functionality is based on another new API presented in WWDC 2022, the Transferable protocol. Fetching photos, videos, or other assets, such as live photos or bursts takes just a few lines of code thanks to that, something that becomes even faster when combined with the new async/await APIs. Overall, using PhotosPicker is a simple task for the majority of use cases. In this post you’ll meet how to present PhotosPicker in SwiftUI, and how to load items from the photo library once users have finished selecting them.
Presenting the PhotosPicker view
Usually, most views are part of the SwiftUI framework, however that’s not the case with photos picker. To make it available and start using it, import the PhotosUI
framework:
1 2 3 |
import PhotosUI |
At its simplest form, the following code demonstrates what we need in order to trigger the appearance of the PhotosPicker
view in SwiftUI:
1 2 3 4 5 |
PhotosPicker(selection: $selectedItem) { Label(“Fetch Photos”, systemImage: “photo.fill.on.rectangle.fill”) } |
The first argument is the binding value of either a state or published property that will store the selected item by the user:
1 2 3 |
@State var selectedItem: PhotosPickerItem? |
PhotosPickerItem
, the type of the selectedItem
property, is a type that conforms to Transferable
protocol, and that will help us get the selected item easily in a while.
The second argument in the PhotosPicker
view is a label; this can be any SwiftUI content you desire, exactly just like the label of a button. In fact, PhotosPicker
is a button with a predefined action; to present the actual photos picker when pressed. To keep things simple here, a Label view with some text and a SF Symbol image is just enough.
Obviously, we can treat PhotosPicker
like any other button and style it with view modifiers as necessary. For instance, we can update the default button style and its foreground color:
1 2 3 4 5 6 7 |
PhotosPicker(selection: $selectedItem) { Label(“Fetch Photos”, systemImage: “photo.fill.on.rectangle.fill”) } .buttonStyle(.plain) .foregroundColor(.indigo) |
When the PhotosPicker
view gets pressed the actual picker will show up. There, and depending on how we configure the picker, users can select either one or several photos or other assets they want and add them to the app.
Filtering the displayed content
There are more arguments that the PhotosPicker
initializer can get, with the most common being a filter regarding the kind of assets the picker is going to display. For example, the value supplied to the matching
argument next will make the picker display only images and nothing else:
1 2 3 4 5 6 |
PhotosPicker(selection: $selectedItem, matching: .images) { Label(“Fetch Photos”, systemImage: “photo.fill.on.rectangle.fill”) } |
images
is a static property in a struct called PHPickerFilter
, and it’s just one among several properties that PHPickerFilter
contains. Have a look at the official documentation page to see them all.
It’s interesting that besides single values, we can also combine filters in order to let the picker show different kind of media. For instance, the following will make the picker display all videos and live photos found in the device’s library. Notice that in this case filters are elements of an array, and that array is an argument to the any(of:)
method:
1 2 3 4 5 6 |
PhotosPicker(selection: $selectedItem, matching: .any(of: [.videos, .livePhotos])) { Label(“Fetch Photos”, systemImage: “photo.fill.on.rectangle.fill”) } |
Providing additional arguments
When making apps that let users import photos or other media from the photo library, then it might be useful and meaningful to limit the number of items that can be selected. You might want to allow picking just one photo, or up to two videos, or an arbitrary number of live photos. No matter what the occasion might be, PhotosPicker
view makes it really straightforward to set a limit to the number of selected items:
1 2 3 4 5 6 7 8 |
PhotosPicker(selection: $selectedItems, maxSelectionCount: 5, matching: .images ) { Label(“Fetch Photos”, systemImage: “photo.fill.on.rectangle.fill”) } |
The code you see here allows users to select up to five photos when the picker will show up. To keep the default settings, simply avoid providing any value to the maxSelectionCount
argument.
???? Important Note: You have probably noticed that in the previous code the binding value is named selectedItems
instead of selectedItem
, indicating the use of an array. Indeed, and you can call this a rule, the given binding value must be an array of PhotosPickerItem
objects ([PhotosPickerItem]
) mandatorily when you need to let users select more than one items. Otherwise, the first selected item will make the picker disappear without giving the possibility to pick additional items. I’m covering the case of selecting multiple items later on in this post.
It might be also meaningful to keep the order users select photos or other items sometimes. To manage that, give the value .ordered
to the selectionBehavior
argument:
1 2 3 4 5 6 7 8 |
PhotosPicker(selection: $selectedItems, selectionBehavior: .ordered, matching: .images ) { Label(“Fetch Photos”, systemImage: “photo.fill.on.rectangle.fill”) } |
In this case, selected items will show badges that indicate the order of selection instead of badges with a checkmark.
Lastly, there is one more argument that we can supply PhotosPicker
view initializer with, called preferredItemEncoding
. It’s recommended by the documentation to leave its value to current
and avoid transcoding of images and videos.
1 2 3 4 5 6 7 8 |
PhotosPicker(selection: $selectedItem, matching: .images, preferredItemEncoding: .current ) { Label(“Fetch Photos”, systemImage: “photo.fill.on.rectangle.fill”) } |
Most of the times you won’t need to even deal with that argument, so just skip it. As a final note, all arguments presented so far can be put together in the same initializer so PhotosPicker
can be configured any way we like it.
Fetching the selected item
Having seen how to configure PhotosPicker
, it’s now time to find out how to get the selected items once the user is done with the picker. In most of the previous examples, there is the selectedItem
binding value passed as argument, and this is where we’ll get the selected item from.
We want to fetch the actual item once the value of selectedItem
has changed, so we need to observe for changes taking place in it. In SwiftUI we do that using the onChange(of:perform:)
view modifier. You may read more about it here.
1 2 3 4 5 |
.onChange(of: selectedItem) { newItem in } |
The newItem
parameter value of the closure contains the selected item by the user, and similarly to selectedItem
, it is also a PhotosPickerItem
value. The latter conforms to Transferable
protocol, which in turn provides us with a couple of methods to get the transferred items; in this case the selected media.
The easiest way to fetch whatever the user has selected is by calling an asynchronous method (async
) named loadTransferable(type:)
, accessible through the newItem
object. When invoking this method it’s mandatory to prepend it with the await
keyword. On top of that, loadTransferable(type:)
is a throwing method, so we also need to either use a try-catch
statement, or optionally unwrap its return value using the try?
keyword.
To keep things as simple as possible we’ll avoid using a do-catch
here, but we’ll unwrap everything using a guard let
statement like so instead:
1 2 3 4 5 6 7 8 |
.onChange(of: selectedItem) { newItem in Task { guard let imageData = try? await newItem?.loadTransferable(type: Data.self) else { return } // Use the imageData… } } |
Note a few things here:
- It’s mandatory to include everything in a
Task
since we’re performing an asynchronous operation. - There is no guarantee that
newItem
will always have a value; it can benil
, so we treat it as an optional. - The argument in the
loadTransferable(type:)
method is the type that we need to convert transferred data to. Here, aData
object.
If everything goes normally and no errors occur along the way, then the imageData
will contain the data of the selected item. Supposing that we’re fetching images, then we can initialize a UIImage
object with that data like so:
1 2 3 4 5 6 7 8 9 10 |
.onChange(of: selectedItem) { newItem in Task { guard let imageData = try? await newItem?.loadTransferable(type: Data.self) else { return } // Create a UIImage object from the imageData. selectedPhoto = UIImage(data: imageData) } } |
The above constitutes the fastest and easiest way to fetch the selected item from the picker. The selectedPhoto
property used here is another state property (it can also be a @Published
property in a view model) that stores the fetched UIImage:
1 2 3 |
@State var selectedPhoto: UIImage? |
We can now put everything together, fetch an image, and then display it using an Image view:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
struct ContentView: View { @State var selectedItem: PhotosPickerItem? @State var selectedPhoto: UIImage? var body: some View { VStack { if let photo = selectedPhoto { Image(uiImage: photo) .resizable() .scaledToFit() .frame(width: 300, height: 300) } PhotosPicker(selection: $selectedItem, matching: .images) { Label(“Fetch Photos”, systemImage: “photo.fill.on.rectangle.fill”) } } .onChange(of: selectedItem) { newItem in Task { guard let imageData = try? await newItem?.loadTransferable(type: Data.self) else { return } selectedPhoto = UIImage(data: imageData) } } } } |
Getting an Image instead of a UIImage
Just right above I demonstrated how to get a UIImage
object from the selected image in the picker. You might find doing so unnecessary, and consider getting a SwiftUI Image
object directly as a more useful approach. You can definitely change what I showed and do exactly that:
1 2 3 4 5 6 7 |
.onChange(of: selectedItem) { newItem in Task { selectedImage = try? await newItem?.loadTransferable(type: Image.self) } } |
Here, the selectedImage
is the following:
1 2 3 |
@State var selectedImage: Image? |
selectedImage
will either get an actual value if unwrapping everything won’t fail and not any errors will occur, or it will remain nil otherwise. To display it, simply modify the sample code shown in the previous part like so:
1 2 3 4 5 6 7 8 |
if let image = selectedImage { image .resizable() .scaledToFit() .frame(width: 300, height: 300) } |
However, there is an important downside to note here. According to the documentation, getting an Image
instance from the loadTransferable(type:)
method works only for png images. The code illustrated here won’t work for other type of photos, such as jpeg or gif.
If that’s prohibiting to you, then getting the selected item as a UIImage
is an one-way road. If your purpose is to let users fetch png images only, then getting an Image
object as shown in this part will work fine.
Fetching multiple items
There are a few required changes in the code you’ve seen so far when it’s necessary to let users choose multiple elements in the photos picker. The starting and pretty important point is the binding value we provide the PhotosPicker
initializer with; it has to be an array of PhotoPickerItem
objects:
1 2 3 4 5 6 |
PhotosPicker(selection: $selectedItems, matching: .images) { Label(“Fetch Photos”, systemImage: “photo.fill.on.rectangle.fill”) } |
In this case, selectedItems
is declared like so:
1 2 3 |
@State var selectedItems = [PhotosPickerItem]() |
Without any other configuration, users can pick as many photos as they want. Apart from the above, however, the way we fetch selected items must be adapted as well.
Using the onChange(of:perform:)
modifier once again, what we have to do is pretty much the same to what it’s demonstrated in previous parts. But with one difference; we begin to work with arrays and not single objects:
1 2 3 4 5 6 7 |
.onChange(of: selectedItems) { newItems in newItems.forEach { item in } } |
newItems
is an array and we’re running through its elements using the forEach(_:)
higher order function. For every single item contained in the item
parameter value of the closure, we’ll perform actions that we’ve talked about already:
- We’ll create a new
Task
in order to fetch each image asynchronously. - Using the
loadTranferable(type:)
we’ll get the data of each image. - We’ll convert that data to an
UIImage
object.
There is though an addition as well. That is, every resulting UIImage
object will be appended into an array. All these are shown in the following code segment:
1 2 3 4 5 6 7 8 9 10 11 |
.onChange(of: selectedItems) { newItems in newItems.forEach { item in Task { guard let data = try? await item.loadTransferable(type: Data.self) else { return } guard let image = UIImage(data: data) else { return } selectedPhotos.append(image) } } } |
selectedPhotos
is an array of UIImage
objects, declared as a @State
property like this:
1 2 3 |
@State var selectedPhotos = [UIImage]() |
Once again, let’s put everything together in order to demonstrate how multiple items can be fetched using the photos picker:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
struct ContentView: View { @State var selectedItems = [PhotosPickerItem]() @State var selectedPhotos = [UIImage]() var body: some View { VStack { if !selectedPhotos.isEmpty { ScrollView(showsIndicators: false) { ForEach(selectedPhotos, id: \.self) { photo in Image(uiImage: photo) .resizable() .scaledToFit() .frame(width: 300, height: 300) } } .frame(width: 300, height: 500) } PhotosPicker(selection: $selectedItems, matching: .images) { Label(“Fetch Photos”, systemImage: “photo.fill.on.rectangle.fill”) } } .onChange(of: selectedItems) { newItems in newItems.forEach { item in Task { guard let data = try? await item.loadTransferable(type: Data.self) else { return } guard let image = UIImage(data: data) else { return } selectedPhotos.append(image) } } } } } |
Conclusion
Undeniably photos picker had been a missing native element in SwiftUI. However it comes with a downside, which is no support for older system versions. PhotosPicker API is available in iOS 16 and above, and macOS 13 and above. For previous systems, it’s still necessary to resort to UIViewControllerRepresentable implementations and use PHPicker APIs.
What I’ve shown in this post is how to configure photos picker, and how to fetch single or multiple selected items using the fastest and easiest approach. But that’s not all, as there are still topics not covered here, such as how to get selected items as files, how to manage loading progress, how to deal with thumbnails, and more. All these may be the subject of future posts. Until then, though, thanks for reading and take care! ????