100 Days of SwiftUI Day 57

100 Days of SwiftUI – Day 57

We’ve arrived at day 57 of the 100 Days of SwiftUI! Yesterday, we completed a few challenges for the Bookworm app as we wrapped up that project. Today, we’re diving into a new technique project, where we’ll expand on Core Data, which is an essential part of developing with Swift. Let’s dive in!

Why does .self work for ForEach?

When using .self with an object like a Book, Swift will use the entire object to identify it.

When we use \.self as an identifier, we mean “the whole object”, but in practice that doesn’t mean much – a struct is a struct, so it doesn’t have any sort of specific identifying information other than its contents. So what actually happens is that Swift computes the hash value of the struct, which is a way of representing complex data in fixed-size values, then uses that hash as an identifier.

Hacking with Swift, Paul Hudson (@twostraws)

Internally, when a Hash value is created, it means that the data we’re working with conforms to the Hashable protocol. For example, if we create a struct that conforms to the Hashable protocol, we need to ensure that all it’s properties also conform to Hashable, otherwise using .self will not work.

//
//  ContentView.swift
//  CoreDataProject


import SwiftUI

struct Student: Hashable {
    let name: String
}

struct ContentView: View {
    let students = [Student(name: "Harry Potter"), Student(name: "Hermione Granger")]
    
    var body: some View {
        List(students, id: \.self) { student in
            Text(student.name)
        }
    }
}

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

An important note: if you calculate a Hash, quit the app, run it again and recalculate the Hash, the values might be different. That’s why we should not save Hash values for later use.

Creating NSManagedObject subclasses in SwiftUI

When we create a new Core Data entity, Xcode automatically generates a managed object class (moc) we can use in our code. We can then use a @FetchRequest to pull data into our UI. However, this requires us to use nil coalescing everywhere, which is not ideal. There is a way around this, by creating NSManagedObject classes ourselves instead of letting Core Data do it for us.

To accomplish this, you can use the data model editor and set the entity’s Codegen to manual/none. Check out Paul’s video to see exactly how.

//
//  Movie+CoreDataProperties.swift
//  CoreDataProject

import Foundation
import CoreData


extension Movie {

    @nonobjc public class func fetchRequest() -> NSFetchRequest<Movie> {
        return NSFetchRequest<Movie>(entityName: "Movie")
    }

    @NSManaged public var title: String?
    @NSManaged public var director: String?
    @NSManaged public var year: Int16
    
    public var wrappedTitle: String {
        title ?? "Unknown Title" // this will nil coalesce the title everywhere in our code automatically
    }

}

extension Movie : Identifiable {

}

Conditional saving of NSManagedObjectContext

We’ve seen how we can save changes to our moc in the Bookworm app. However, instead of just calling save, wouldn’t it be better to only call save if there are actual changes? Of course it would! Luckily, it’s very easy to do so:

//
//  ContentView.swift
//  CoreDataProject


import SwiftUI

struct ContentView: View {
    @Environment(\.managedObjectContext) var moc
    var body: some View {
        Button("Save") {
            if moc.hasChanges { // if there are changes in our moc, only then save.
                try? moc.save()
            }
        }
    }
}

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

Ensuring Core Data objects are unique using constraints

By default, Core Data will add any data you want. To streamline this and ensure only unique data is saved, Core Data uses constraints. When using constraints, we can give Core Data all the data we want and it will resolve any duplicates for us.

//
//  DataController.swift


import CoreData // Imports CoreData functions
import Foundation

class DataController: ObservableObject {
    let container = NSPersistentContainer(name: "CoreDataProject") // Tell CoreData to use our CoreDataProject 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)")
                return
            }
            
            self.container.viewContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump // merges duplicate objects based on their properties
        }
    }
}

Wrap up

And that’s it for day 57! Tomorrow, we’ll continue the technique project and we’ll learn about NSPredicate, changing fetch requests dynamically, creating relationships, and more. Stay tuned for that!

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 20 – Guess the Flag

100 Days of SwiftUI – Day 4 – Type Annotations

100 Days of SwiftUI – Day 37

100 Days of SwiftUI – Day 69