Your Second iOS & SwiftUI App

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

Part 3: Managing Rows

22. Challenge: New Book Sheet

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: 21. ForEach Next episode: 23. Environment

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: 22. Challenge: New Book Sheet

So! You’ve got a view that says “Add New Book”! Alas, it doesn’t actually do that. Yet!

You challenge is to make it so that tapping on that view will bring you to this view: where you can fill out the information for a new book to be added to your library. Let’s have it be a modal sheet, just like your photo picker.

Don’t worry about actually adding the book to your model yet—-we’ll take care of that in the next episode. But at the end of this challenge, you should have all the data for a new book ready to be supplied.

Two tips for you: It’ll make it a lot easier on you if you can edit the title and author of a book while you’re creating one. So I recommend changing them from being constants, like they currently are. Those served us well, but no longer.

And, notice how the whole bottom part of this screen is exactly the same as what’s in your detail view. I bet you can come up with a good way to reuse that.

Have fun!

Did you have fun? I hope so. I did! Let me detail that fun.

When you hit control and shift, you can get multiple cursors! So I used that ability, combined with the arrow keys to select both of my “Published vars”.

@Published var 
@Published var 

Then I copied those on top of my two lets, in a single operation.

class Book: ObservableObject {
  @Published var title: String
  @Published var author: String
  @Published var microReview: String

And then I hit escape to go back to one cursor. Armed with a fully editable Book type, I kicked off a NewBookView.

When creating a New Book View, I’d start off with a book with all empty strings.

struct NewBookView: View {
  var book = Book(title: "", author: "")

  var body: some View {

And as Book was an ObservableObject now, ObservedObject was the right property wrapper for it.

@ObservedObject var book = Book(title: "", author: "")

I replaced the placeholder Text with a VStack.

  var body: some View {
    VStack {
      
    }
  }

And in the Content closure, added textfield for the book’s title.

    VStack {
      TextField("Title", text: $book.title)
    }

Then, I added another one, for the Author.

      TextField("Title", text: $book.title)
      TextField("Author", text: $book.author)
    }

And I padded them away from the edge.

    })
    .padding()
  }

Then I went to assess what I could grab to fill out the rest of the NewBookView, from DetailView.

Basically, it was this VStack.

So I extracted that to a subview, named ReviewAndImageStack.

      }

      ReviewAndImageStack()

      Spacer()

Really, that bottom Spacer belonged with the Stack, though.

      }

      ReviewAndImageStack()
    }

⬇️

        updateButton
      }

      Spacer()
    }

Same with the sheet and confirmation dialog.

      ReviewAndImageStack()
    }
    .padding()
  }
}

⬇️

      Spacer()
    }
    .sheet(isPresented: $showingImagePicker) {
      PHPickerViewController.View(image: $image)
    }
    .confirmationDialog(isPresented: $showingAlert) {
    }
  }
}

Those State variables needed to move into the new view, so I cut and pasted them.

  @Binding var image: UIImage?

  var body: some View {

⬇️

struct ReviewAndImageStack: View {
  @State var showingImagePicker = false
  @State var showingDialog = false

  var body: some View {

And the other two properties needed to be forwarded along, so I copied and pasted them, instead.

struct ReviewAndImageStack: View {
  @ObservedObject var book: Book
  @Binding var image: UIImage?
  @State var showingImagePicker = false

I also had to pass them in, using the initializer.

ReviewAndImageStack(book: book, image: $image)

Because I was going to reuse ReviewAndImageStack in another file, too, I wanted to move it out of this one.

I could have put it in the Book views file, but I thought it was complex enough to warrant its own file. So I copied the name, and made that.

Then I went back to DetailView, cut ReviewAndImageStack, and pasted it over the auto-generated code in the new file.

I also needed to move the PHPicker import.

/// THE SOFTWARE.

import class PhotosUI.PHPickerViewController
import SwiftUI

And to get a preview going, I used a default book and nil image binding.

ReviewAndImageStack(book: .init(), image: .constant(nil)

That looked better with some horizontal padding…

    ReviewAndImageStack(book: .init(), image: .constant(nil))
      .padding(.horizontal)
  }

…and why not preview it in both color schemes?––like all the other views!

      .padding(.horizontal)
      .previewedInAllColorSchemes
  }

Now I was ready to get back to my new book view.

I’d be needing a temporary optional Image binding for the new book. I used a State variable for that.

  @ObservedObject var book = Book(title: "", author: "")
  @State var image: Image? = nil

  var body: some View {

Then I passed the properties to a ReviewAndImageStack instance.

      TextField("Author", text: $book.author)
      ReviewAndImageStack(book: book, image: $image)
    })

I spaced things out a little bit, with 24 points…

VStack(spacing: 24, content: {

And my NewBookView was complete. I just had to bring it up as a sheet back in ContentView!

To do that, I’d need a Boolean binding.

I called it adding new book, and set it to false to start off with.

struct ContentView: View {
  @State var addingNewBook = false
  @State var library = Library()

The place to set it to true was our new button’s action.

        Button {
          addingNewBook = true
        } label: {

I added a sheet modifier, right on that button–which was going to launch it.

        .padding(.vertical, 8)
        .sheet(isPresented: /*@START_MENU_TOKEN@*//*@PLACEHOLDER=Is Presented@*/.constant(false)/*@END_MENU_TOKEN@*/, content: {
          /*@START_MENU_TOKEN@*//*@PLACEHOLDER=Content@*/Text("Sheet Content")/*@END_MENU_TOKEN@*/
        })

        ForEach(library.sortedBooks) { book in

I bound the bool to it…

.sheet(isPresented: $addingNewBook, content: {

I didn’t need the onDismiss closure, so I deleted thar, and I already have a name for the content closure I needed: NewBookView-dot-init.

        .sheet(
          isPresented: $addingNewBook,
          content: NewBookView.init
        )

        ForEach(library.sortedBooks) { book in

Then I could Live preview and try it! And tapping on the new book button took me to the new view…

and while creating a book here wouldn’t do anything, yet, I could at least dismiss the sheet by dragging down.