Leave a rating/review
The next feature we’re going to work on will let users add their own images to the app from the Detail View.
There are lots of possible ways to go about this, but we’re going to use one of Apple’s built-in photo pickers. This will let a user choose images from their own photo library on their device! Over the next two episodes we’ll work towards that goal with four steps:
First, set up a data structure to store the photos within the app. As I mentioned earlier, a dictionary will be a good match!
Second, employ Apple’s PHPicker to get access to the Photo library and present it in a modal sheet from the Detail View.
Third, connect the storage you set up in step 1, through all of the UI, to the photo picker
And finally update our Book Image type to show an image if there is one, and fall back to the SF symbol placeholder if there isn’t.
To start off, Step 1, we’re going to create that dictionary I mentioned earlier to store our book images. We’re going to be using SwiftUI’s Image type for that. So, in Library.swift, import SwiftUI at the top.
import SwiftUI
And then add a new images variable - a dictionary that uses Books as keys, and SwiftUI Images as values. We have to specify the type because we’re going to start with an empty one and can’t rely on type inference.
]
var images: [Book: Image] = [:]
}
As you see from this error, Books can’t be dictionary keys, because they don’t adopt the Hashable protocol. We can fix this over in Book.swift.
The Swift compiler can auto-generate what Book needs to conform to Hashable, all we have to do is tell Swift that we want Book to adopt that protocol. Just write a colon, then the name of the protocol.
struct Book: Hashable {
It’s a bit of a simplification, but you can think of something “Hashable” as being “uniquely identifiable”. Which is why dictionaries require their keys to be Hashable. In a dictionary, you can’t have two values for the same key, because each key is unique.
If you have a structure whose properties are all themselves Hashable, then the compiler considers the struct to conform to Hashable as well. Strings are Hashable, so now, your Book is also Hashable. Let’s head to ContentView.
A nice side effect of making Book Hashable, is you can now just use the entire book as its own id, instead of picking an arbitrary one of its properties.
List(Library().sortedBooks, id: \.self) { book in
And now, onto Step 2 - where to get the images…
This PHPickerViewController View file, which I wrote for the Starter folder, is the first part of the solution. If you’re working from your own project from following along, you should copy this file into your project now.
SwiftUI doesn’t have a native photo picker yet, so we need to use one from UIKit. That’s this PHPicker, from the PhotosUI framework.
It’s not that difficult to use SwiftUI and UIKit together, but it is outside the scope of this course.
If you’d like to learn about it, looking up this UIViewControllerRepresentable is the place to begin. I’ll also leave some links to other courses can books that can help you out.
All you need to know, in order to use everything here, is right in the top part of the file:
extension PHPickerViewController {
struct View {
@Binding var image: UIImage?
}
}
You’ll need to instantiate a PHPickerViewController View. And, you’ll need to bind its image property to an optional Image.
The typical way to present an image picker on iOS is with a modal view.
If you haven’t heard that term before, it’s a type of view that covers up at least some portion of your main UI. And until you interact with it in a meaningful way, or, dismiss it, you won’t be able to interact with the main UI again.
And so, you’re in a different “mode” of your app. Hence: modal.
In SwiftUI, there’s a type of modal view which takes up almost the entire screen. It’s called a “sheet”. On iPhones, it just leaves a little sliver of another view, behind it, at the top.
You can add one to a view using the sheet modifier. A sheet requires a Bool Binding to know whether it should be presented. And one way to provide that Binding is by way of a State variable.
We’re going to present this modal view from the Detail View, so let’s start there. Again, we’re going to use a State variable to say whether the modal is showing or not. showingImagePicker is a good name, I think. Add that as a DetailView property.
let book: Book
@State var showingImagePicker
var body: some View {
To start off with, the modal won’t be presented. So, this should be false.
@State var modalIsPresented👉 = false
We’ll need a way to change it to true when we want to add a photo. A button can do that for us! We’ll add it stacked below the Book Image. Start by embedding in a VStack…
)
VStack {
Book.Image(title: book.title)
}
Spacer()
Then start a button with the initializer that takes a title and an action.
Book.Image(title: book.title)
Button(<#T##title: StringProtocol##StringProtocol#>, action: <#T##() -> Void#>)
}
Title it “Update Image”. If you’d like an ellipsis afterwards, hold option and hit semicolon.
Button("Update Image…", action: <#T##() -> Void#>)
For the action, when you press this button, set showingImagePicker to true.
Book.Image(title: book.title)
Button("Update Image…") {
showingImagePicker = true
}
}
And let’s add some padding around that button as well.
showingImagePicker = true
}
.padding()
}
Now we can add the sheet!bHit shift-command-L, and type “sheet” in the modifiers tab. Drag one of those onto the bottom of the canvas, to add it to the VStack.
.padding()
.sheet(isPresented: /*@START_MENU_TOKEN@*//*@PLACEHOLDER=Is Presented@*/.constant(false)/*@END_MENU_TOKEN@*/) {
/*@START_MENU_TOKEN@*//*@PLACEHOLDER=Content@*/Text("Sheet Content")/*@END_MENU_TOKEN@*/
}
With showingImagePicker already set up, you’ve got access to the Binding you need.
.sheet(isPresented: 👉$showingImagePicker) {
For “Content”, this is where the PHPicker view will go! First, you’ll need to import PHPickerViewController at the top, which is a class from the PhotosUI framework.
/// THE SOFTWARE.
import class PhotosUI.PHPickerViewController
import SwiftUI
Notice that we can import just the class we need instead of the entire framework. Then back in the sheet’s Content closure, you can try instantiating one of those views we looked at… “PHPickerViewController.View”
.sheet(isPresented: $showingImagePicker) {
PHPickerViewController.View(image: <#T##Binding<Image?>#>)
}
…and you’re reminded that you need an optional Image binding. Let’s have the DetailView store, and supply one of those with a new optional Image variable. Don’t forget the Binding attribute in front!
struct DetailView: View {
let book: Book
@Binding var image: Image?
@State var showingImagePicker = false
Now we can pass the binding to the picker view. Remember to use the dollar sign prefix, which you almost always need, for Bindings.
PHPickerViewController.View(image: $image)
For the preview, the supplied Image can be nil, but we still need to wrap that “absence of an Image” in a Binding. Binding’s constant method is just the thing you need, for that.
DetailView(book: .init(), image: .constant(nil))
This is all we need for our picker UI to work in DetailView, but it looks like I’ve got errors elsewhere that are crashing my previews.