Your Second iOS & SwiftUI App

Nov 4 2021 · Swift 5.5, iOS 15, Xcode 13

Part 2: Data Flow

12. Image Binding

Episode complete

Play next episode

Next
About this episode

Leave a rating/review

See forum comments
Cinema mode Mark complete Download course materials
Previous episode: 11. Modal Views Next episode: 13. Color Schemes

Get immediate access to this and 4,000+ other videos and books.

Take your career further with a Kodeco Personal Plan. With unlimited access to over 40+ books and 4,000+ professional videos in a single subscription, it's simply the best investment you can make in your development career.

Learn more Already a subscriber? Sign in.

Transcript: 12. Image Binding

At this point, Detail View is set up. When the Photo picker is launched, it will be able to pass a chosen image back through its binding property. But where will it go? Right now it ends in Detail View!

We want it to be stored in our Library, and there’s already an instance of Library in Content View. So we need to get this Binding to an image from Detail View back to Content View.

The compiler is going to help us out here. We’ve made Detail View dependent on having a binding passed in, but that binding has to come from somewhere.

So we can follow the string of errors until we get to the Library! Right now, it looks like our error is in BookRow because that is where Detail Views are being created! But it isn’t where our Library instance is.

So, again, store a Binding to an optional Image in BookRow…

  let book: Book
  @Binding var image: Image?

  var body: some View {

…and pass it along to the DetailView.

destination: DetailView(book: book, image: $image)

Now we’ve got a new error, because we need to pass a binding to BookRow… And there, right above the BookRow instance is our Library instance! But we don’t have access to the Library instance, just its sorted books.

So, let’s pull this Library out into a variable that ContentView will keep track of for us. And use the variable in the List instead of making a new Library there

struct ContentView: View {
  var library = Library()

  var body: some View {
    NavigationView {
      List(library.sortedBooks, id: \.self) { book in

The library is going to act as our source of truth for the book and image data in this app. So we need it to be a State variable.

@State var library = Library()

That’s what will allow us to bind to something within it, and leave this State variable to keep track of its value. That “something” we need to bind is the dictionary value for a row’s book.

      List(library.sortedBooks, id: \.self) { book in
        BookRow(
          book: book,
          image: $library.images[book]
        )
      }

With that, we can give it a try!

Start the live preview.

Once I get to a detail view, the button sets DetailView’s showingImagePicker to true, and either choosing an image, or hitting Cancel, sets it back to false.

The images you select are being added to your library! But you might not believe it, because they’re not showing up. Stop the live preview, and hop over to Book views and find our Book image type.

The reason the images you’re adding to your library are not showing up is Book Images currently only display symbols. That’s all we’ve told them to do so far!

  • So, let’s supply them with optional SwiftUI Images. You can call the constant image.
  struct Image: View {
    let image: SwiftUI.Image?
    let title: String

We’ve got errors now because the struct’s auto-generated member-wise initializer is asking for an optional Image. So all of these instances down in the preview think they’re missing something.

To get rid of those errors, we could just pass nil into all of these instances. But instead, let’s keep the preview code the same, and add an initializer to Book Image, just for making a reusable preview.

With a struct, if you want to keep the memberwise initializer that Swift generates for you AND add a new one, the new initializer needs to be in an extension.

So extend Book.Image just below its definition, and let’s say with a triple-slash what this initializer will be for.

extension Book.Image {
  /// A preview Image.
}

Then start a new init with a title String parameter.

  /// A preview Image.
  init(title: String) {

  }
}

And pass that title along to the memberwise initializer that requires an Image, with nil for the image.

  init(title: String) {
    self.init(
      image: nil,
      title: title
    )
  }

That should get rid of your preview errors, we’ll tackle the other one crashing the preview in a minute.

Because we’ve got a little work left to do in Book Image. What we want to find out now, is if we have a SwiftUI Image to work with. You can start that off with if let syntax, based on image.

    var body: some View {
      if let image = Image {
      let symbol =

Everything that was in body before will move into the else clause.

    var body: some View {
      if let image = image {

      } else {
        let symbol =
          SwiftUI.Image(title: title)
            ....
          .foregroundColor(.secondary)
      }
    }

Now if there definitely IS an image, let’s resize that to fill…

      if let image = image {
        image
          .resizable()
          .scaledToFill()
      } else {

…and give it the same frame the symbol would have.

          .scaledToFill()
          .frame(width: size, height: size)
      } else {

Now, let’s go fix that error in BookRow.

Just pass it the value of the image binding. (That means not using the dollar sign prefix.) Because a book row isn’t going to change the image at all, just display it.

      HStack {
        Book.Image(image: image, title: book.title, size: 80)
        
        TitleAndAuthorStack(

And in DetailView…

…same idea. Just pass in the image.

      VStack {
        Book.Image(image: image, title: book.title)

        Button("Update Image…") {

Let’s try the live preview now.

Now, the images from your library are showing up! They just look kind of terrible! That’s okay; let’s work on it.

Add one last property to Book Image: a cornerRadius.

    var size: CGFloat?
    let cornerRadius: CGFloat

    var body: some View {

And apply that when you have an image.

          .frame(width: size, height: size)
          .cornerRadius(cornerRadius)
      } else {

The preview won’t be showing that, so it doesn’t matter what you use. You can just use “dot init” to create a default CGFloat.

  /// A preview Image.
  init(title: String) {
    self.init(
      uiImage: nil,
      title: title,
      cornerRadius: .init()
    )

Like it says, that’s actually zero, but we don’t care. We just need a non-optional value.

In DetailView…

…try a corner radius of 16…

        Book.Image(
          uiImage: image,
          title: book.title,
          cornerRadius: 16
        )

(Also, scale that to fit.)

          cornerRadius: 16
        )
        .scaledToFit()

And in BookRow, use 12.

          size: 80,
          cornerRadius: 12
        )

Live preview, one last time!

Now we’re talking. Note that the image picker we’re using doesn’t support camera usage. It’s only for the Photos library. But PHPicker was new for iOS 14, with no real updates for iOS 15. But hopefully it’ll have fewer limitations in coming years.

Also, there’s no way to get custom images into this picker, using the preview devices. You’ll need to build and run for that.

And if we’re going to bother to do that, we ought to do a few other things to spruce up the appearance of this app.