We’ve arrived at day 55 of the 100 Days of SwiftUI! Yesterday, we started with the implementation of the Bookworm app as we dove into Core Data. Today, we’re implementing the second and final part of the app. Let’s dive in!
Showing book details
When the user taps a book in ContentView
we’re going to present a detail view with some more information – the genre of the book, their brief review, and more. We’re also going to reuse our new RatingView
, and even customize it.
//
// DetailView.swift
// Bookworm
import SwiftUI
struct DetailView: View {
let book: Book
var body: some View {
ScrollView {
ZStack(alignment: .bottomTrailing) {
Image(book.genre ?? "Fantasy")
.resizable()
.scaledToFit()
Text(book.genre?.uppercased() ?? "FANTASY")
.font(.caption)
.fontWeight(.black)
.padding(8)
.foregroundColor(.white)
.background(.black.opacity(0.75))
.clipShape(Capsule())
.offset(x: -5, y: -5)
}
Text(book.author ?? "Unknown Author")
.font(.title)
.foregroundColor(.secondary)
Text(book.review ?? "No review")
.padding()
RatingView(rating: .constant(Int(book.rating)))
.font(.largeTitle)
}
.navigationTitle(book.title ?? "Unknown Book")
.navigationBarTitleDisplayMode(.inline)
}
}
Sorting fetch requests with SortDescriptor
When you use SwiftUI’s
@FetchRequest
property wrapper to pull objects out of Core Data, you get to specify how you want the data to be sorted – should it be alphabetically by one of the fields? Or numerically with the highest numbers first? We specified an empty array, which might work OK for a handful of items but after 20 or so will just annoy the user.In this project we have various fields that might be useful for sorting purposes: the title of the book, the author, or the rating are all sensible and would be good choices, but I suspect title is probably the most common so let’s use that.
Fetch request sorting is performed using a new type called
Hacking with Swift, Paul Hudson (@twostraws)SortDescriptor
, and we can create them from either one or two values: the attribute we want to sort on, and optionally whether it should be reversed or not.
@FetchRequest(sortDescriptors: [SortDescriptor(\.title)]) // sorts by title
Deleting from a Core Data fetch request
We already used @FetchRequest
to place Core Data objects into a SwiftUI List
, and with only a little more work we can enable both swipe to delete and a dedicated Edit/Done button.
//
// 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: [SortDescriptor(\.title), SortDescriptor(\.author)]) var books: FetchedResults<Book> // reads out all the books we've added so far, sorted by title
@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 {
DetailView(book: book)
} 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)
}
}
}
}
.onDelete(perform: deleteBooks)
}
.navigationTitle("Bookworm")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
EditButton()
}
ToolbarItem(placement: .navigationBarTrailing) {
Button {
showingAddScreen.toggle()
} label: {
Label("Add Book", systemImage: "plus")
}
}
}
.sheet(isPresented: $showingAddScreen) {
AddBookView()
}
}
}
func deleteBooks(at offsets: IndexSet) {
for offset in offsets { // Loop through all the offsets given
let book = books[offset] // Find the book in the fetch request
moc.delete(book) // book is now the object we want to delete, which we can then delete
}
// try? moc.save() // try saving the moc
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Using an alert to pop a NavigationLink programmatically in SwiftUI
You’ve already seen how
Hacking with Swift, Paul Hudson (@twostraws)NavigationLink
lets us push to a detail screen, which might be a custom view or one of SwiftUI’s built-in types such asText
orImage
. Because we’re inside aNavigationView
, iOS automatically provides a “Back” button to let users get back to the previous screen, and they can also swipe from the left edge to go back. However, sometimes it’s useful to programmatically go back – i.e., to move back to the previous screen when we want rather than when the user swipes.
//
// DetailView.swift
// Bookworm
import SwiftUI
struct DetailView: View {
let book: Book
@Environment(\.managedObjectContext) var moc
@Environment(\.dismiss) var dismiss
@State private var showingDeleteAlert = false
var body: some View {
ScrollView {
ZStack(alignment: .bottomTrailing) {
Image(book.genre ?? "Fantasy")
.resizable()
.scaledToFit()
Text(book.genre?.uppercased() ?? "FANTASY")
.font(.caption)
.fontWeight(.black)
.padding(8)
.foregroundColor(.white)
.background(.black.opacity(0.75))
.clipShape(Capsule())
.offset(x: -5, y: -5)
}
Text(book.author ?? "Unknown Author")
.font(.title)
.foregroundColor(.secondary)
Text(book.review ?? "No review")
.padding()
RatingView(rating: .constant(Int(book.rating)))
.font(.largeTitle)
}
.navigationTitle(book.title ?? "Unknown Book")
.navigationBarTitleDisplayMode(.inline)
.alert("Delete Book", isPresented: $showingDeleteAlert) {
Button("Delete", role: .destructive, action: deleteBook)
Button("Cancel", role: .cancel) {}
} message: {
Text("Are you sure?")
}
.toolbar {
Button {
showingDeleteAlert = true
} label: {
Label("Delete this book", systemImage: "trash")
}
}
}
func deleteBook() {
moc.delete(book)
// try? moc.save()
dismiss()
}
}
Wrap up
And that’s it for day 55! We’ve now completed another project and learned the basics of working with Core Data. Great stuff! Tomorrow, we’ll wrap up this project by reviewing what we’ve learned and completing a few challenges. Time to recharge!
100 Days of SwiftUI – Day 55