Photo Picker in SwiftUI (PhotosPicker)

Photo Picker in SwiftUI (PhotosPicker)

·

4 min read

In this post, we’ll implement a SwiftUI view that allows the users to pick a photo from their photo library, with the SwiftUI PhotosPicker.

The source code of this post is available here.

Step 0 - Overview

Before getting started, let’s learn more about PhotosPicker. It is a public structure of type SwiftUI view; users can select one or multiple photos from their library via this picker. Note that this is a struct in PhotosUIand is available on devices with iOS 16, macOS 13, watchOS 9 or higher.

Step 1 - Import PhotosUI

As mentioned in the overview, the PhotosPicker is part of the PhotosUI so let’s create a SwiftUI project and add the import expression above the ContentView structure.

import SwiftUI
import PhotosUI

struct ContentView: View {
    var body: some View {
    //...
    }
}

Step 2 - Add a state property and the PhotosPicker

Add a @State property called selected of type [PhotosPickerItem].

Now in the VStack of the body of the ContentView, let’s declare a PhotoPicker element. The parameters of the initializer are explained below:

  • selection: Binding<[PhotosPickerItem]> Here we need to pass the state property selected declared as instructed above. Since this is a binding array of PhotosPickerItem, the item(s) selected in the picker will be updated to the selected array in our view.

  • maxSelectionCount: Int? This is an optional of Integer, which sets the maximum number of items that the user can pick from the library.

  • matching filter: PHPickerFilter? This parameter is a PHPickerFilter, which is a struct in PhotosUI that represents the type of elements (images or videos that can be stored and picked from a photo library) and acceptable values included: .images, .videos, .screenshots etc. In this demo, we’ll use .images to get a photo.

  • @ViewBuilder label: Label In short this parameter is where we define the front end of the picker. For instance, in this demo, we passed in a VStack with a photo icon and a text message "Select a photo".

struct ContentView: View {
    @State var selected: [PhotosPickerItem] = []
    var body: some View {
        VStack {
            PhotosPicker(selection: $selected, maxSelectionCount: 1, matching: .images) {
                VStack {
                    Image(systemName: "photo")
                        .resizable()
                        .frame(width: 60, height: 45, alignment: .center)
                    Text("Select a photo")
                }
            }
        }
        .padding()
    }
}

Once this step is done, you should be able to see a picker and on tap gesture, a picker view will be presented, where you can select the default images in the simulator or your photos if you’re running this project on a physical device.

Step 3 - Handle changes in the selection

Now you might be wondering, we have a state property to hold the selected item(s) from the photo library, how can the users see the images after selecting them?

We’re now addressing that. The state property selected is an array of PhotosPickerItem and need to fetch the data from it first. When do we need to perform this fetch? Every time the user changes the selection from the picker, right? Hence, we need an onChange modifier to observe the updates in selected.

First, add another state property to hold the data fetched from the selected items. We can make it an optional so that failure to parse the image data can be handled.

@State var data: Data?

Then add the on change modifier as shown below:

Every time when selected gets an update, the code will try to read the selected array to check if there is at least one item. If no item is found, no further action will be performed. (Logic of the guard else statement.)

If at least one item is found, it invokes the function .loadTransferable to parse the selected photo item and transfer the item to data. In the completion handler, we use a switch to handle both successful and failing cases. If the transfer fails, here only an error message gets printed. If the transfer goes through, we use if let to avoid nil value in data. If not nil, the data gets assigned to the state property data in the view.

PhotosPicker(selection: $selected, maxSelectionCount: 1, matching: .images) {
    VStack {
        //...
    }
}
//-------new lines below
.onChange(of: selected) { newValue in
    guard let selectedItem = selected.first else {
        return
    }
    selectedItem.loadTransferable(type: Data.self) { result in
        switch result {
        case .success(let data):
            if let data = data {
                self.data = data
            } else {
                print("Found nil in data")
            }
        case .failure(let error):
            print(error.localizedDescription)
        }
    }
}

Step 4 - Present the selected image

Since we got the data of the image in the view now, we’re ready to present the selected image in the view.

Add the following code in the ContentView VStack, above the PhotosPicker.

The if let statement double checks if the data is non-nil, if not, an UIKit UIImage element will be initiated with the data.

Inside the if statement, we can add a text like "Selected photo:" as a hint of what this image is. The image below the text is declared with the UIImage element. You can use a fit aspectRatio to make the image size fit the view more.

if let data = data, let uiImage = UIImage(data: data) {
    VStack {
        HStack {
            Text("Selected photo: ")
                .bold()
            Spacer()
        }
        Image(uiImage: uiImage)
            .resizable()
            .aspectRatio(contentMode: .fit)
    }
    .padding()
}
Spacer()
    .frame(height: 50)