100 Days of SwiftUI Day 55

100 Days of SwiftUI – Day 55

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 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.

Hacking with Swift, Paul Hudson (@twostraws)
@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 NavigationLink lets us push to a detail screen, which might be a custom view or one of SwiftUI’s built-in types such as Text or Image. Because we’re inside a NavigationView, 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.

Hacking with Swift, Paul Hudson (@twostraws)
//
//  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!

Darryl

Hi! My name is Darryl and this is my personal blog where I write about my journey as I learn programming! You'll also find articles about other things that interest me including games, tech and anime.

Post navigation

Leave a Comment

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

100 Days of SwiftUI – Day 11 – Structs, Part 2

100 Days of SwiftUI – Day 95

100 Days of SwiftUI – Day 86

100 Days of SwiftUI – Day 28 – BetterRest Wrap Up