Your Second iOS & SwiftUI App

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

Part 3: Managing Rows

23. Environment

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: 22. Challenge: New Book Sheet Next episode: 24. Environment Values

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: 23. Environment

Coming off of that challenge, ready to go with a view that allows for creation of a book, let’s write a method that will add that new book to our library.

  var sortedBooks: [Book] { booksCache }

  func addNewBook(_ book: Book)

  /// An in-memory cache of the manually-sorted books that are persistently stored.

That book may optionally come with an image.

  func addNewBook(_ book: Book, image: Image?) {

  }

In the Editor-slash-Structure menu…

…there’s a command to Add Documentation. I use it a lot, so I always use the keyboard shortcut instead: option-command-forward slash

  var sortedBooks: [Book] { booksCache }

  /// <#Description#>
  /// - Parameters:
  ///   - book: <#book description#>
  ///   - image: <#image description#>
  func addNewBook(_ book: Book, image: UIImage?) {
    booksCache.insert(book, at: 0)
    uiImages[book] = image
  }

I think the parameters are clear enough without extra documentation…

  var sortedBooks: [Book] { booksCache }

  /// <#Description#>
  func addNewBook(_ book: Book, image: UIImage?) {

…but let’s add a description. Soon, we’re going to be able to manually sort our books. This method is going to add one at the start of all of those books.

  var sortedBooks: [Book] { booksCache }

  /// Adds a new book at the start of the library's manually-sorted books.
  func addNewBook(_ book: Book, image: Image?) {

Structured Documentation is how you get useful information to show up when you option-click on your code.

Now we’ll be able to get this quick reference, from other files. Try inserting the book parameters at the beginning of the cache.

  func addNewBook(_ book: Book, image: UIImage?) {
    booksCache.insert(book, at: 0)
  }

Before fixing that error, let’s also try to set the image for the book.

    booksCache.insert(book, at: 0)
    uiImages[book] = image
  }

The errors are there because Library is a struct. One way to get rid of them is to mark the method as “mutating”.

mutating func addNewBook(_ book: Book, image: UIImage?) {

But we’re about to find it much more helpful for Library to be a class.

class Library {

So no, mutating is not what we need.

  var sortedBooks: [Book] { booksCache }

  func addNewBook(_ book: Book, image: UIImage?) {

And as you’ve seen with Book, when you change a struct to a class, in SwiftUI, you very likely should make it an Observable Object, too.

class Library: ObservableObject {

The properties that you mutate are again, very likely, going to need to be “Published”.

@Published private var booksCache: [Book] = [
@Published var uiImages: [Book: UIImage] = [:]

That way, your UI can update in response to those mutations. In NewBookView, let’s add a button that will call this new method.

The place we’ll put that button is a toolbar. For that, you’ll need the “toolbar” modifier:

    .padding()
    .toolbar {

    }
  }

And then, you’ll need a Toolbar Item.

    .toolbar {
      ToolbarItem(placement: <#T##ToolbarItemPlacement#>, content: <#T##() -> _#>)
    }

There are lots of placement options for toolbar items, but we’ll go with “status”.

    .toolbar {
      ToolbarItem(placement: .status) {

      }
    }

Let’s give the item an Add To Library button, so you can see what “status” does.

        ToolbarItem(placement: .status) {
          Button("Add to Library") {

          }
        }

In order to get any toolbar to show up, you need to wrap it in a Navigation View. I’ll wrap it in a Stack of some kind with an option-command-click, and then update it to “NavigationView”

  var body: some View {
    NavigationView {
      VStack(spacing: 24) {

Now, you can see your button showing up at the bottom of the screen, in a toolbar! If you use “status”, as we are, your toolbar item will be centered.

And seeing as how we have a navigation bar now, let’s give it a fun title to describe what this view is for, like, “Got a new book?”

      .padding()
      .navigationTitle("Got a new book?")
      .toolbar {

Now, to add to a library object, we’ll need a reference to one. And we could achieve that, with an ObservedObject variable.

  @State var image: UIImage? = nil
  @ObservedObject var library: Library

  var body: some View {

But considering the same library instance is going to be used throughout the entire app, SwiftUI offers a better property wrapper for the job: EnvironmentObject.

@EnvironmentObject var library: Library

I’ll explain more about how that works, in a second. First, let’s add a new book to our library!

      trailing: Button("Add") {
        library.addNewBook(book, image: image)
      }

To see that in action, try the live preview!

Crash! That’s okay! It’s completely expected.

A NewBookView requires a library object. But the preview doesn’t have one to work with. To solve that, use the environmentObject modifier.

NewBookView().environmentObject(<#T##object: ObservableObject##ObservableObject#>)

That provides a view with a single instance of an observable object, to use. Pass in a library object.

NewBookView().environmentObject(Library())

And now, your preview won’t crash!

But, adding it to a library doesn’t do anything useful, on this screen. Let’s head to ContentView.

Instead of creating a library with State, let’s have it use the same “Environment library” that everything else will be using, shortly.

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

  var body: some View {

You’ll need the same environmentObject code in the preview here, as well.

struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    ContentView()
      .environmentObject(Library())
      .previewedInAllColorSchemes
  }
}

Try the live preview from this screen.

And, success! There’s a new book in our library! The way that works is this library created in the ContentView preview…

    ContentView()
      .environmentObject(Library())
      .previewedInAllColorSchemes

…is passed along to any other view that code in ContentView launches. (As long as those other views use the EnvironmentObject wrapper.)

We can improve the experience of adding a book, a bit, but let’s finish our Environment Object work first. Try building and running the app.

It will crash immediately, and tell you why. And the key here is “an ancestor of this view.” Now, the root problem is the same as you saw in the crashing New Book preview.

And you solve that, in a preview…

    ContentView()
      .environmentObject(Library())

…by providing a library to the previewed instance. But where is ContentView being instantiated, when running the actual app?

I actually showed you when we first made the project! It’s here, in ReadMeApp.

ContentView()

The WindowGroup in ReadMeApp is ContentView’s only ancestor. There’s no difference, from what you did in your previews, in what you need to do for the full, running app. Just give the app’s Content view a library to work with.

ContentView().environmentObject(Library())

And now your app won’t crash!

There are two other places where using a library environment object can simplify your code, at this point.

One is in DetailView. Instead of an Image binding, switch to a library.

  @ObservedObject var book: Book
  @EnvironmentObject var library: Library

  var body: some View {

Get rid of the image argument in the preview, and don’t forget to add a library in its place.

  static var previews: some View {
    DetailView(book: .init())
      .environmentObject(Library())
      .previewedInAllColorSchemes
  }

Then, you can provide a binding to the correct Image, using the new library property.

ReviewAndImageStack(book: book, image: $library.images[book])

And you should have an error in BookRow…

…which can be solved by deleting the now-unneeded image argument there, as well.

destination: DetailView(book: book)

Make the same switch from Image Binding, to library, in BookRow.

  @ObservedObject var book: Book
  @EnvironmentObject var library: Library

  var body: some View {

Fix the error below, by looking up an image in the library, for the row’s book…

uiImage: library.images[book],

…and fix the one above, by getting rid of yet another “image” argument.

        ForEach(library.sortedBooks) { book in
          BookRow(book: book)
        }

Feel free to try the app out one more time to double check your work, but everything should be running smoothly.