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
Hacking with Swift, Paul Hudson (@twostraws)\.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.
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!
100 Days of SwiftUI – Day 57