WWDC23 SALE: Save 50% on all my Swift books and bundles! >>

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, TextEditor, 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 {                
            TextEditor(text: $review)

            Picker("Rating", selection: $rating) {
                ForEach(0..<6) {
                    Text(String($0))
                }
            }                
        } header: {
            Text("Write a review")
        }

        Section {
            Button("Save") {
                // add the book
            }
        }
    }
    .navigationTitle("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: moc)
newBook.id = UUID()
newBook.title = title
newBook.author = author
newBook.rating = Int16(rating)
newBook.genre = genre
newBook.review = review

try? 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 – in the toolbar, in this case – to toggle that property.
  3. A sheet() modifier that shows AddBookView when the property becomes true.

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

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

@State private var showingAddScreen = false

That gives us a managed object context we can use later on to delete books, 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 as needed.

Replace the existing body property of ContentView with this:

 NavigationView {
    Text("Count: \(books.count)")
        .navigationTitle("Bookworm")
        .toolbar {
            ToolbarItem(placement: .navigationBarTrailing) {
                Button {
                    showingAddScreen.toggle()
                } label: {
                    Label("Add Book", systemImage: "plus")
                }
            }
        }
        .sheet(isPresented: $showingAddScreen) {
            AddBookView()
        }
}

Tip: That explicitly specifies a trailing navigation bar placement so that we can add a second button later.

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 be able to dismiss the current view:

@Environment(\.dismiss) var dismiss

Finally, add a call to dismiss() to the end of your button’s action closure.

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.

Save 50% in my WWDC23 sale.

SAVE 50% To celebrate WWDC23, all our books and bundles are half price, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.

Save 50% on all our books and bundles!

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI 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 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 Beyond Code

Was this page useful? Let us know!

Average rating: 4.7/5

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.