It’s day 54 of the 100 Days of SwiftUI! Today, we’re starting with the implementation of our Bookworm app. Yesterday, we were introduced to Core Data, Swift’s framework for databases. That’ll be on display today as well, so let’s dive in!
Creating books with Core Data in SwiftUI
The first task in this project is to design a Core Data model for our books. With that model in place, we can create a SwiftUI view
to create new books in our database.

//
// AddBookView.swift
// Bookworm
//
import SwiftUI
struct AddBookView: View {
@Environment(\.managedObjectContext) var moc
@Environment(\.dismiss) var dismiss
@State private var title = ""
@State private var author = ""
@State private var rating = 3
@State private var genre = ""
@State private var review = ""
let genres = ["Fantasy", "Horror", "Kids", "Mystery", "Poetry", "Romance", "Thriller"]
var body: some View {
NavigationView {
Form {
Section {
TextField("Name of book", text: $title)
TextField("Name of author", 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") {
let newBook = Book(context: moc) // create a new book in our managed object context
// Set the properties for the new book
newBook.id = UUID()
newBook.title = title
newBook.author = author
newBook.rating = Int16(rating)
newBook.genre = genre
newBook.review = review
try? moc.save() // save the book to the managed object context
dismiss() // closes the viewe
}
}
}
.navigationTitle("Add Book")
}
}
}
struct AddBookView_Previews: PreviewProvider {
static var previews: some View {
AddBookView()
}
}
//
// ContentView.swift
// Bookworm
import SwiftUI
struct ContentView: View {
@Environment(\.managedObjectContext) var moc // moc to store view context for our data model which we can use to delete books
@FetchRequest(sortDescriptors: []) var books: FetchedResults<Book> // reads out all the books we've added so far
@State private var showingAddScreen = false // Used to show a sheet where we add books
var body: some View {
NavigationView {
Text("Count is: \(books.count)")
.navigationTitle("Bookworm")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
showingAddScreen.toggle()
} label: {
Label("Add Book", systemImage: "plus")
}
}
}
.sheet(isPresented: $showingAddScreen) {
AddBookView()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Adding a custom star rating component
Now that we can add a book to our database, we need a proper way for our user to rate a book. To accomplish this, we are going to build a star rating component that allows users to give a book a 1 to 5 star rating.
//
// RatingView.swift
// Bookworm
//
// Created by Darryl Soerdjpal on 19/08/2022.
//
import SwiftUI
struct RatingView: View {
@Binding var rating: Int
var label = ""
var maximumRating = 5 // maximum number of stars
var offImage: Image? // will use the same as on image
var onImage = Image(systemName: "star.fill") // a filled star image
var offColor = Color.gray // gray stars when not highlighted
var onColor = Color.yellow // yellow stars when highlighted
var body: some View {
HStack {
if label.isEmpty == false {
Text(label)
}
ForEach(1..<maximumRating + 1, id: \.self) { number in
image(for: number)
.foregroundColor(number > rating ? offColor : onColor)
.onTapGesture {
rating = number
}
}
}
}
func image(for number: Int) -> Image {
if number > rating {
return offImage ?? onImage
} else {
return onImage
}
}
}
struct RatingView_Previews: PreviewProvider {
static var previews: some View {
RatingView(rating: .constant(4))
}
}
Building a list with @FetchRequest
Our ContentView
has a @FetchRequest
that requests all the books in the database at once. We’re going to use this to fill our ContentView
with all the books, along with their rating and author.
To flex our coding muscles, we first created a new EmojiRatingView
that will display an emoji based on our rating on the main screen of the app.
//
// EmojiRatingView.swift
// Bookworm
import SwiftUI
struct EmojiRatingView: View {
let rating: Int16
var body: some View {
switch rating {
case 1:
return Text("????")
case 2:
return Text("????")
case 3:
return Text("????")
case 4:
return Text("????")
default:
return Text("????")
}
}
}
struct EmojiRatingView_Previews: PreviewProvider {
static var previews: some View {
EmojiRatingView(rating: 3)
}
}
//
// ContentView.swift
// Bookworm
import SwiftUI
struct ContentView: View {
@Environment(\.managedObjectContext) var moc // moc to store view context for our data model which we can use to delete books
@FetchRequest(sortDescriptors: []) var books: FetchedResults<Book> // reads out all the books we've added so far
@State private var showingAddScreen = false // Used to show a sheet where we add books
var body: some View {
NavigationView {
List {
ForEach(books) { book in
NavigationLink {
Text(book.title ?? "Unknown Title")
} label: {
HStack {
EmojiRatingView(rating: book.rating)
.font(.largeTitle)
VStack(alignment: .leading) {
Text(book.title ?? "Unknown Title")
.font(.headline)
Text(book.author ?? "Unknown Author")
.foregroundColor(.secondary)
}
}
}
}
}
.navigationTitle("Bookworm")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
showingAddScreen.toggle()
} label: {
Label("Add Book", systemImage: "plus")
}
}
}
.sheet(isPresented: $showingAddScreen) {
AddBookView()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Wrap up
And that’s it for day 54! Tomorrow, we’ll finish the implementation of the Bookworm app by learning how to show details from our Core Data model, in this case from books, as well as sorting and deleting @FetchRequests
. Stay tuned for all of that and more tomorrow!
100 Days of SwiftUI – Day 54