NEW: Join my free 100 Days of SwiftUI challenge today! >>

Creating books with Core Data

Paul Hudson    @twostraws   

Our first task in this project will be to design a Core Data model for our books, then creating a new view to add books to the database.

First, the model: open Bookworm.xcdatamodeld and add a new entity called “Book” – we’ll create one new object in there for each book the user has read. In terms of what constitutes a book, I’d like you to add the following attributes:

  • id, UUID – a guaranteed unique identifier we can use to distinguish between books
  • title, String – the title of the book
  • author, String – the name of whoever wrote the book
  • genre, String – one of several strings from the genres in our app
  • review, String – a brief overview of what the user thought of the book
  • rating, Integer 16 – the user’s rating for this book

Most of those should make sense, but the last one is an odd one: “integer 16”. What is the 16? And how come there are also Integer 32 and Integer 64? Well, just like Float and Double the difference is how much data they can store: Integer 16 uses 16 binary digits (“bits”) to store numbers, so it can hold values from -32,768 up to 32,767, whereas Integer 32 uses 32 bits to store numbers, so it hold values from -2,147,483,648 up to 2,147,483,647. As for Integer 64… well, that’s a really large number – about 9 quintillion.

The point is that these values aren’t interchangeable: you can’t take the value from a 64-bit number and try to store it in a 16-bit number, because you’d probably lose data. On the other hand, it’s a waste of space to use 64-bit integers for values we know will always be small. As a result, Core Data gives us the option to choose just how much storage we want.

Our next step is to write a form that can create new entries. This will combine so many of the skills you’ve learned so far: Form, @State, @Environment, TextField, Picker, sheet(), and more, plus all your new Core Data knowledge.

Start by creating a new SwiftUI view called “AddBookView”. In terms of properties, we need an environment property to store our managed object context:

@Environment(\.managedObjectContext) var moc

As this form is going to store all the data required to make up a book, we need @State properties for each of the book’s values except id, which we can generate dynamically. So, add these properties next:

@State private var title = ""
@State private var author = ""
@State private var rating = 3
@State private var genre = ""
@State private var review = ""

Finally, we need one more property to store all possible genre options, so we can make a picker using ForEach. Add this last property to AddBookView now:

let genres = ["Fantasy", "Horror", "Kids", "Mystery", "Poetry", "Romance", "Thriller"]

We can now take a first pass at the form itself – we’ll improve it soon, but this is enough for now. Replace the current body with this:

NavigationView {
    Form {
        Section {
            TextField("Name of book", text: $title)
            TextField("Author's name", text: $author)

            Picker("Genre", selection: $genre) {
                ForEach(genres, id: \.self) {
                    Text($0)
                }
            }
        }

        Section {
            Picker("Rating", selection: $rating) {
                ForEach(0..<6) {
                    Text("\($0)")
                }
            }

            TextField("Write a review", text: $review)
        }

        Section {
            Button("Save") {
                // add the book
            }
        }
    }
    .navigationBarTitle("Add Book")
}

When it comes to filling in the button’s action, we’re going to create an instance of the Book class using our managed object context, copy in all the values from our form (converting rating to an Int16 to match Core Data), then save the managed object context.

Most of this work is just copying one value into another, with the only vaguely interesting thing being how we convert from an Int to an Int16 for the rating. Even that is pretty guessable: Int16(someInt) does it all.

Add this code in place of the // add the book comment:

let newBook = Book(context: self.moc)
newBook.title = self.title
newBook.author = self.author
newBook.rating = Int16(self.rating)
newBook.genre = self.genre
newBook.review = self.review

try? self.moc.save()

That completes the form for now, but we still need a way to show and hide it when books are being added.

Showing AddBookView involves returning to ContentView.swift and following the usual steps for a sheet:

  1. Adding an @State property to track whether the sheet is showing.
  2. Add some sort of button – a navigation bar item, in this case – to toggle that property.
  3. A sheet() modifier that shows AddBookView when the property becomes true.

However, this time there’s a small piece of bonus work and it stems from the way SwiftUI’s environment works. You see, when we place an object into the environment for a view, it becomes accessible to that view and any views that can call it an ancestor. So, if we have View A that contains inside it View B, anything in the environment for View A will also be in the environment for View B. Taking this a step further, if View A happens to be a NavigationView, any views that are pushed onto the navigation stack have that NavigationView as their ancestor so they share the same environment.

Now think about sheets – those are full-screen pop up windows on iOS. Yes, one screen might have caused them to appear, but does that mean the presented view can call the original its ancestor? SwiftUI has an answer, and it’s “no”, which means that when we present a new view as a sheet we need to explicitly pass in a managed object context for it to use. As the new AddBookView will be shown as a sheet from ContentView, we need to add a managed object context property to ContentView so it can be passed in.

Enough talk – let’s start writing some more code. Please start by adding these three properties to ContentView:

@Environment(\.managedObjectContext) var moc
@FetchRequest(entity: Book.entity(), sortDescriptors: []) var books: FetchedResults<Book>

@State private var showingAddScreen = false

That gives us a managed object context we can pass into AddBookView, a fetch request reading all the books we have (so we can test everything worked), and a Boolean that tracks whether the add screen is showing or not.

For the ContentView body, we’re going to use a navigation view so we can add a title plus a button in its top-right corner, but otherwise it will just hold some text showing how many items we have in the books array – just so we can be sure everything is working. Remember, this is where we need to add our sheet() modifier to show an AddBookView, passing in the managed object context so it can write its data.

You’ve already seen how we use the @Environment property wrapper to read values from the environment, but here we need to write values in the environment. This is done using a modifier of the same name, environment(), which takes two parameters: a key to write to, and the value you want to send in. For the key we can just send in the one we’ve been using all along, \.managedObjectContext, and for the value we can pass in our own moc property – we’re effectively just forwarding it on.

Replace the existing body property of ContentView with this:

 NavigationView {
    Text("Count: \(books.count)")
        .navigationBarTitle("Bookworm")
        .navigationBarItems(trailing: Button(action: {
            self.showingAddScreen.toggle()
        }) {
            Image(systemName: "plus")
        })
        .sheet(isPresented: $showingAddScreen) {
            AddBookView().environment(\.managedObjectContext, self.moc)
        }
}

Bear with me – we’re almost done! We’ve now designed our Core Data model, created a form to add data, then updated ContentView so that it can present the form and pass in its managed object context. The final step is to to make the form dismiss itself when the user adds a book.

We’ve done this before, so hopefully you know the drill. We need to start by adding another environment property to AddBookView to track the current presentation mode:

@Environment(\.presentationMode) var presentationMode

Finally, we need to add a call to dismiss() to the end of our button’s action closure, like this:

self.presentationMode.wrappedValue.dismiss()

You should be able to run the app now and add an example book just fine. When AddBookView slides away the count label should update itself to 1.

Tip: Two SwiftUI glitches might affect you while following along, depending on which Xcode version you’re using. The first is that you might find the + button really hard to tap, because whereas UIKit extends the tappable area to make it easier to interact with SwiftUI does not, so you need to tap exactly on the +. The second is that you might find tapping the button only works once. This is definitely a SwiftUI bug because if we toggle the Boolean using an onTapGesture() on the text view then everything works – it only has a hard time when it’s using a navigation bar item. Hopefully this will get resolved soon – perhaps even by the time you follow this!

LEARN SWIFTUI FOR FREE I have a massive, free SwiftUI video collection on YouTube teaching you how to build complete apps with SwiftUI – check it out!

BUY OUR BOOKS
Buy Pro Swift Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift (Vapor Edition) Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Server-Side Swift (Kitura Edition) Buy Beyond Code

Was this page useful? Let us know!

Average rating: 5.0/5