100 Days of SwiftUI Day 53

100 Days of SwiftUI – Day 53

We’ve arrived at day 53 of the 100 Days of SwiftUI! Yesterday, we wrapped up the Cupcake Corner app and completed 3 challenges to improve it even further. Today, we’re diving into a new project called Bookworm. Let’s take a look!

Bookworm app introduction

The bookworm app will be used to track which books a user has read and what they thought of them. They’ll be able to write about it and even add a star rating. In order to accomplish these things, this project will introduce us to Core Data, which is Apple’s framework for working with databases.

Creating a custom component with @Binding

We’ve already seen @State and @StateObject property wrappers. Today, we’re adding a new one, called @Binding: it lets us store a mutable value in a view that actually points to some other value from elsewhere.

//
//  ContentView.swift
//  Bookworm

import SwiftUI

struct PushButton: View {
    let title: String
    @Binding var isOn: Bool // @Binding makes it external state, not local state
    
    var onColors = [Color.red, Color.yellow]
    var offColors = [Color(white: 0.6), Color(white: 0.4)]
    
    var body: some View {
        Button(title) {
            isOn.toggle()
        }
        .padding()
        .background(
            LinearGradient(colors: isOn ? onColors : offColors, startPoint: .top, endPoint: .bottom)
        )
        .foregroundColor(.white)
        .clipShape(Capsule())
        .shadow(radius: isOn ? 0 : 5)
    }
}

struct ContentView: View {
    @State private var rememberMe = false
    
    var body: some View {
        VStack {
            PushButton(title: "Remember me", isOn: $rememberMe) // We're now passing in the binding and not the value 
            Text(rememberMe ? "On" : "Off")
        }
        
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Accepting multi-line text input with TextEditor

We’ve used SwiftUI’s TextField view to get some input from users. For larger pieces of text, however, there’s a different view called TextEditor. It works much the same as the former, but has the additional benefit of allowing multiple lines of text.

//
//  ContentView.swift
//  Bookworm


import SwiftUI

struct ContentView: View {
    @AppStorage("notes") private var notes = "" // Warning: AppStorage is backed by UserDefaults which is not designed to store private or confidential data
    
    var body: some View {
        NavigationView {
            TextEditor(text: $notes)
                .navigationTitle("Notes")
                .padding()
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

How to combine Core Data and SwiftUI

SwiftUI and Core Data were introduced almost a decade apart. Despite this large gap in time, they work together very well thanks to great efforts by Apple.

Core Data is an object graph and persistence framework, which means we can define objects, give properties to those objects and read and write them from permanent storage. This sounds like Codable and UserDefaults, but it’s much more advanced than that. There is virtually no limit to how much data you can store and there are advanced functions for sorting and filtering data, for example.

We start by creating a new data model file and add the necessary attributes.

Creating a new data model

Then, we add the necessary code to ensure we can load or data model and read and write data to and from it.

//
//  DataController.swift
//  Bookworm

import CoreData // Imports CoreData functions
import Foundation

class DataController: ObservableObject {
    let container = NSPersistentContainer(name: "Bookworm") // Tell CoreData to use our Bookworm datamodel. NSPersistentContainer is the actual data being saved to the device
    
    init() {
        container.loadPersistentStores { description, error in
            if let error = error {
                print("Core Data failed to load: \(error.localizedDescription)")
            }
        }
    }
}
//
//  BookwormApp.swift
//  Bookworm


import SwiftUI

@main

struct BookwormApp: App {
    @StateObject private var dataController = DataController() // instance of the DataController class
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\.managedObjectContext, dataController.container.viewContext) // We add the DataController to our SwiftUI environment. A managedObjectContext is essentially the live version of your data, which exists in memory until you actually write it to the disk/storage.
        }
    }
}
//
//  ContentView.swift
//  Bookworm


import SwiftUI

struct ContentView: View {
    @Environment(\.managedObjectContext) var moc
    @FetchRequest(sortDescriptors: []) var students: FetchedResults<Student> // We're saying: make a new FetchRequest with no sorting, put in a property called students with a type of FetchedResults Student.
    
    var body: some View {
        VStack {
            List(students) { student in
                Text(student.name ?? "Unknown") // Optionals in Core Data mean something different than optionals in SwiftUI, despite having the same name. Core Data always returns optionals, even if marked as required. You have to use nil coalescing everywhere, but we'll learn a better workaround in the future.
            }
            
            Button("Add") {
                let firstNames = ["Ginny", "Harry", "Hermione", "Luna", "Ron"]
                let lastNames = ["Granger", "Lovegood", "Potter", "Weasley"]
                
                let chosenFirstName = firstNames.randomElement() ?? "Unknown"
                let chosenLastName = lastNames.randomElement() ?? "Unknown"
                
                let student = Student(context: moc)
                student.id = UUID()
                student.name = "\(chosenFirstName) \(chosenLastName)"
                
                try? moc.save() // This can throw an error, but we don't do any error handling here because we've hardcoded everything above
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Wrap up

And that’s it for day 53! That was quite a lot of learning with a lot of new stuff thrown at us. It was very interesting though! We’ll dive into more detailed ways of using Core Data soon enough, but for now, this was a great introduction. Tomorrow, we’ll be starting the implementation of the Bookworm app. 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 38

100 Days of SwiftUI – Day 25 – Rock, Paper, Scissors

100 Days of SwiftUI – Day 67

100 Days of SwiftUI – Day 46