New in SwiftUI 4 : PhotosPicker

DevTechie Inc
Dec 7, 2022

Introduced in iOS 16 with SwiftUI 4, came the most Swifty way to present photo picker for the app users.

Before iOS 16, it was possible to present photo picker with the help of PHPickerViewController from the world of UIKit and by bringing them to SwiftUI world using UIViewRepresentable.

Let’s build an example to understand this better.

We will start with an import statement because PhotosPicker lives in the PhotosUI framework.

import PhotosUI
Next, we need to add a state variable of PhotosPickerItem type so we can hold user selected photo.

@State private var selectedItem: PhotosPickerItem?
Let’s work on our view where we will have an Image view to display selected image by the user along with a button to launch photo picker.

At a minimum, PhotosPicker takes selection parameter where we can pass binding value of PhotosPickerItem type, in our case that’s the state variable called selectedItem. It also has a ViewBuilder trailing closure for the label and we will use Label view for that.

import PhotosUI
import SwiftUIstruct ContentView: View {
    @State private var selectedItem: PhotosPickerItem?
    
    var body: some View {
        VStack {
                Image(systemName: "photo.artframe")
                    .resizable()
                    .frame(width: 200, height: 200)
                    .foregroundColor(.gray)
                    .padding()
            
            PhotosPicker(selection: $selectedItem) {
                Label("Select a photo DevTechie", systemImage: "photo.artframe")
            }
        }
    }
}
Output:

Tapping on PhotosPicker, launches the photos UI for us to pick a photo. If we pick a photo, view simply dismisses. At this point we have a PhotosPickerItem but we haven’t used it yet so let’s do that now.

Display selected Image
In order to display user selected image, we will have to process PhotosPickerItem and for that we need to observe the event when user has made the selection.

This can be done easily with the help on onChange modifier. We attach this modifier to our selectedItem state variable and when the variable will change, we will get the new value in closure.

PhotosPicker(selection: $selectedItem) {
    Label("Select a photo DevTechie", systemImage: "photo.artframe")
}
.onChange(of: selectedItem) { newValue in
    
}
We also need another state variable to store parsed image from the PhotosPickerItem, so we will add following Statevariable right after selectedItem State variable.

@State private var selectedItem: PhotosPickerItem?
@State private var selectedImage: Image?
We also want to display our image so for that, we will add a conditional block inside the VStack to make sure that we display selected image when its present otherwise display photo.artframe systemImage.

if let selectedImage {
    selectedImage
        .resizable()
        .frame(width: 200, height: 200)
        .foregroundColor(.gray)
        .padding()
} else {
    Image(systemName: "photo.artframe")
        .resizable()
        .frame(width: 200, height: 200)
        .foregroundColor(.gray)
        .padding()
}
Now is the time to process image. We want to run this processing asynchronously so we will use Task block for that purpose.

Task: a task represents a unit of asynchronous work. When we create an instance of Task, we provide a closure that contains the work for that task to perform.
PhotosPickerItem stores image in the form of transferable data so we will use loadTransferable(type:) function to get image data and convert it into an UIImage, which later on will be used to create an Image view.

.onChange(of: selectedItem) { newValue in
    Task {
        if let imageData = try? await newValue?.loadTransferable(type: Data.self), let image = UIImage(data: imageData) {
            selectedImage = Image(uiImage: image)
        }
    }
}
Complete code:

import PhotosUI
import SwiftUIstruct ContentView: View {
    @State private var selectedItem: PhotosPickerItem?
    @State private var selectedImage: Image?
    
    var body: some View {
        VStack {
            if let selectedImage {
                selectedImage
                    .resizable()
                    .frame(width: 200, height: 200)
                    .foregroundColor(.gray)
                    .padding()
            } else {
                Image(systemName: "photo.artframe")
                    .resizable()
                    .frame(width: 200, height: 200)
                    .foregroundColor(.gray)
                    .padding()
            }
            
            PhotosPicker(selection: $selectedItem) {
                Label("Select a photo DevTechie", systemImage: "photo.artframe")
            }
            .onChange(of: selectedItem) { newValue in
                Task {
                    if let imageData = try? await newValue?.loadTransferable(type: Data.self), let image = UIImage(data: imageData) {
                        selectedImage = Image(uiImage: image)
                    }
                }
            }
        }
    }
}
Build and run:

Filtering Photo Library
Photo library contains photos, Live Photos, videos etc. What if we want to handle still images for our app? We can do that with the help of another parameter that PhotosPicker provides, called matching its a PHPickerFilter type which let’s us define that we want to include or exclude.

To test this, let’s include some sample videos into the simulator. You can include your own or download from https://www.pexels.com

Our app doesn’t filter our videos at this point:

Let’s fix that by including matching parameter.

PhotosPicker(selection: $selectedItem, matching: .images)
Notice that videos are gone.

Let’s see the use of .any filter as it can be very powerful. With any filter matching option, we can tell PhotosPicker to show any that are part of an array,

PhotosPicker(selection: $selectedItem, matching: .any(of: [.images, .videos]))
Or we can combine .not filter:

PhotosPicker(selection: $selectedItem, matching: .any(of: [.images, .not(.videos)]))
Multiple Photos
PhotosPicker provides capability to select multiple photos as well so let’s extend our app to handle multi-selection.

In order to support multiple images, we have to make a few changes so let’s start from the top.

Both our State variables, now need to support multiple values so let’s change them to an array of those types.

We are also gonna change selectedImages type from Image to UIImage because we need to iterate over images in view and Image view doesn’t conforms to Hashable protocol.

@State private var selectedItems: [PhotosPickerItem] = []
@State private var selectedImages: [UIImage] = []
Next, we will change view’s if condition to following:

if selectedImages.count > 0 {
We will add a horizontal ScrollView along with a HStack to display all the images in selectedImages variable.

if selectedImages.count > 0 {
    ScrollView(.horizontal) {
        HStack {
            ForEach(selectedImages, id: \.self) { img in
                Image(uiImage: img)
                    .resizable()
                    .frame(width: 200, height: 200)            }
        }
    }
}
We will change onChange modifier to include for loop because now, we will be getting multiple newValues. We will also take this opportunity to empty selectedImages array so we can avoid duplicate images.

.onChange(of: selectedItems) { newValues in
    Task {
        selectedImages = []
        for value in newValues {
            if let imageData = try? await value.loadTransferable(type: Data.self), let image = UIImage(data: imageData) {
                selectedImages.append(image)
            }
        }
    }
}
With all these changes our final code will look like this:

struct ContentView: View {
    @State private var selectedItems: [PhotosPickerItem] = []
    @State private var selectedImages: [UIImage] = []
    
    var body: some View {
        VStack {
            if selectedImages.count > 0 {
                ScrollView(.horizontal) {
                    HStack {
                        ForEach(selectedImages, id: \.self) { img in
                            Image(uiImage: img)
                                .resizable()
                                .frame(width: 200, height: 200)
                            
                        }
                    }
                }
            } else {
                Image(systemName: "photo.artframe")
                    .resizable()
                    .frame(width: 200, height: 200)
                    .foregroundColor(.gray)
                    .padding()
            }
            
            PhotosPicker(selection: $selectedItems, matching: .any(of: [.images, .not(.videos)])) {
                Label("Select a photo DevTechie", systemImage: "photo.artframe")
            }
            .onChange(of: selectedItems) { newValues in
                Task {
                    selectedImages = []
                    for value in newValues {
                        if let imageData = try? await value.loadTransferable(type: Data.self), let image = UIImage(data: imageData) {
                            selectedImages.append(image)
                        }
                    }
                }
            }
        }
    }
}
Build and run:

We can limit number items that can be selected by specifying maxSelectionCount parameter.

PhotosPicker(selection: $selectedItems, maxSelectionCount: 2, matching: .any(of: [.images, .not(.videos)])) {Label("Select a photo DevTechie", systemImage: "photo.artframe")}
Complete code:

import PhotosUI
import SwiftUIstruct ContentView: View {
    @State private var selectedItems: [PhotosPickerItem] = []
    @State private var selectedImages: [UIImage] = []
    
    var body: some View {
        VStack {
            if selectedImages.count > 0 {
                ScrollView(.horizontal) {
                    HStack {
                        ForEach(selectedImages, id: \.self) { img in
                            Image(uiImage: img)
                                .resizable()
                                .frame(width: 200, height: 200)
                            
                        }
                    }
                }
            } else {
                Image(systemName: "photo.artframe")
                    .resizable()
                    .frame(width: 200, height: 200)
                    .foregroundColor(.gray)
                    .padding()
            }
            
            PhotosPicker(selection: $selectedItems, maxSelectionCount: 2, matching: .any(of: [.images, .not(.videos)])) {
                Label("Select a photo DevTechie", systemImage: "photo.artframe")
            }
            .onChange(of: selectedItems) { newValues in
                Task {
                    selectedImages = []
                    for value in newValues {
                        if let imageData = try? await value.loadTransferable(type: Data.self), let image = UIImage(data: imageData) {
                            selectedImages.append(image)
                        }
                    }
                }
            }
        }
    }
}
Build and run for one more time:

With that we have reached the end of this article. Thank you once again for reading. Don’t forget to follow 😍. Also subscribe our newsletter at https://www.devtechie.com