NEW: Join my free 100 Days of SwiftUI challenge today! >>

Importing an image into SwiftUI using UIImagePickerController

Paul Hudson    @twostraws   

In order to bring this project to life, we need to let the user select a photo from their library, then display it in ContentView. I’ve already shown you how this all works, so now it’s just a matter of putting it into our app – hopefully it will make a little more sense this time!

Start by making a new Swift file called ImagePicker.swift, replace its “Foundation” import with “SwiftUI”, then give it this basic struct:

struct ImagePicker: UIViewControllerRepresentable {
    @Environment(\.presentationMode) var presentationMode
    @Binding var image: UIImage?

    func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
        let picker = UIImagePickerController()
        return picker
    }

    func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {

    }
}

If you recall, using UIViewControllerRepresentable means that ImagePicker is already a SwiftUI view that we can place inside our view hierarchy. In this instance we’re wrapping UIKit’s UIImagePickerController, which lets the user select something from their photo library.

When that ImagePicker struct is created, SwiftUI will automatically call its makeUIViewController() method, which is what goes on to create and send back a UIImagePickerController. However, our code doesn’t actually respond to any events inside the image picker – the user can search for an image and select it to dismiss the view, but we don’t then do anything with it.

Rather than making us create a subclass of UIImagePickerController, UIKit instead uses a system of delegation: we create a custom class that will be told when something interesting happened. Each delegate class will usually need to conform to one or more protocols, and in our case that means UINavigationControllerDelegate and UIImagePickerControllerDelegate. The delegates work much like real-life delegates – if you delegate work to someone else, it means you’re giving it to them to complete.

SwiftUI handles these delegate classes by letting us define a coordinator that belongs to the struct. This class can do anything we need, including acting as the delegate for UIKit components, and we can then pass any relevant information back up to the ImagePicker that owns it.

Start by adding this as a nested class inside ImagePicker:

class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
    let parent: ImagePicker

    init(_ parent: ImagePicker) {
        self.parent = parent
    }
}

You can see that conforms to the two protocols we need to use for working with UIKit’s image picker, and also inherits from NSObject which is the base class for most types that come from UIKit.

Because our coordinator class conforms to the UIImagePickerControllerDelegate protocol, we can make it the delegate of the UIKit image picker by modifying makeUIViewController() to this:

func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
    let picker = UIImagePickerController()
    picker.delegate = context.coordinator
    return picker
}

We need to make two more changes to ImagePicker to make it useful. The first is to add a makeCoordinator() method that tells SwiftUI to use the Coordinator class for the ImagePicker coordinator. From our perspective this is obvious, because we created a class called Coordinator that was inside the ImagePicker struct, but this makeCoordinator() method lets us control how the coordinator is made.

If you recall, we gave the Coordinator class a single property: let parent: ImagePicker. This means we need to create it with a reference to the image picker that owns it, so the coordinator can forward on interesting events. So, inside our makeCoordinator() method we’ll create a Coordinator object and pass in self.

Add this method to the ImagePicker struct now:

func makeCoordinator() -> Coordinator {
    Coordinator(self)
}

The final step for ImagePicker is to give the coordinator some sort of functionality. The UIImagePickerController class looks for two methods, but here we’re only going to use one: didFinishPickingMediaWithInfo. This will be called when the user has selected an image, and will be given a dictionary of information about the selected image.

To make ImagePicker useful we need to implement that method inside Coordinator, make it set the image property of its parent ImagePicker, then dismiss the view.

UIKit’s method name is long and complex, so it’s best written using code completion. Make some space inside the Coordinator class and type “didFinishPicking”, then press return to have Xcode fill in the whole method for you. Now modify it to have this code:

func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
    if let uiImage = info[.originalImage] as? UIImage {
        parent.image = uiImage
    }

    parent.presentationMode.wrappedValue.dismiss()
}

That completes ImagePicker.swift, so please head back to ContentView.swift so we can make use of it.

First we need an @State Boolean to track whether our image picker is being shown or not, so start by adding this to ContentView:

@State private var showingImagePicker = false

Second, we need to set that Boolean to true when the big gray rectangle is tapped, so replace the // select an image comment with this:

self.showingImagePicker = true

Third, we need a property that will store the image the user selected. We gave the ImagePicker struct an @Binding property attached to a UIImage, which means when we create the image picker we need to pass in a UIImage for it to link to. When the @Binding property changes, the external value changes as well, which lets us read the value.

So, add this property to ContentView:

@State private var inputImage: UIImage?

Fourth, we need a method that will be called when the ImagePicker view has been dismissed. For now this will just place the selected image directly into the UI, so please add this method to ContentView now:

func loadImage() {
    guard let inputImage = inputImage else { return }
    image = Image(uiImage: inputImage)
}

And finally, we need to add a sheet() modifier somewhere in ContentView. This will use showingImagePicker as its condition, will reference loadImage as its onDismiss parameter, and present an ImagePicker bound to inputImage as its contents.

So, add this directly below the existing navigationBarTitle() modifier:

.sheet(isPresented: $showingImagePicker, onDismiss: loadImage) {
    ImagePicker(image: self.$inputImage)
}

That completes all the steps required to wrap a UIKit view controller for use inside SwiftUI. We went over it a little faster this time but hopefully it still all made sense!

Go ahead and run the app again, and you should be able to tap the gray rectangle to import a picture, and when you’ve found one it will appear inside our UI.

Tip: The ImagePicker view we just made is completely reusable – you can put that Swift file to one side and use it on other projects easily. If you think about it, all the complexity of wrapping the view is contained inside ImagePicker.swift, which means if you do choose to use it elsewhere it’s just a matter of showing a sheet and binding an image.

LEARN SWIFTUI FOR FREE I have a massive, free SwiftUI video collection on YouTube teaching you how to build complete apps with SwiftUI – check it out!

BUY OUR BOOKS
Buy Pro Swift Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift (Vapor Edition) Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Server-Side Swift (Kitura Edition) Buy Beyond Code

Was this page useful? Let us know!

Average rating: 5.0/5