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.
SPONSORED From January 26th to 31st you can join a FREE crash course for iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a senior developer!
Sponsor Hacking with Swift and reach the world's largest Swift community!
Link copied to your pasteboard.