Earlier when we defined places we could use Combine in our Jokes app, we determined that fetching a list of saved jokes would be a great piece of functionality to show off the powers of Combine - but we need to have data in the database before we can do that!
So let’s take a look at the Core Data model which is already defined in the xcdatamodeld file. The JokeManagedObject entity defines the Joke in Core Data,
and as you can see from the Codegen field in the Properties Pane, Core Data will generate a class definition for the entity automatically. We’ll add some extension methods that will deal with saving and deleting jokes.
Before we get into the demo though, one thing to point out: the main data structure we use in the project to represent a joke is aptly named Joke, but it is a struct
Core Data, however, generates _class definitions for you to use in your code. Therefore, JokeManagedObject is a class.
This means we can’t directly save our Jokes to Core Data,
and instead need to translate them to JokeManagedObject objects. This translation will actually comprise a large portion of the save method. Let’s take a look in a demo.
Make a new file in the Models folder and call it JokeManagedObject+.swift. Replace the file contents with the following:
import Foundation
import SwiftUI
import CoreData
import ChuckNorrisJokesModel
extension JokeManagedObject {
static func save(joke: Joke, inViewContext viewContext: NSManagedObjectContext) {
guard joke.id != "error" else { return }
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(
entityName: String(describing: JokeManagedObject.self))
fetchRequest.predicate = NSPredicate(format: "id = %@", joke.id)
if let results = try? viewContext.fetch(fetchRequest),
let existing = results.first as? JokeManagedObject {
existing.value = joke.value
existing.categories = joke.categories as NSArray
existing.languageCode = joke.languageCode
existing.translationLanguageCode = joke.translationLanguageCode
existing.translatedValue = joke.translatedValue
} else {
let newJoke = self.init(context: viewContext)
newJoke.id = joke.id
newJoke.value = joke.value
newJoke.categories = joke.categories as NSArray
newJoke.languageCode = joke.languageCode
newJoke.translationLanguageCode = joke.translationLanguageCode
newJoke.translatedValue = joke.translatedValue
}
do {
try viewContext.save()
} catch {
fatalError("\(#file), \(#function), \(error.localizedDescription)")
}
}
}
Let’s break this down. The save function takes in two parameters: a Joke struct, and a viewContext, which is a NSManagedObjectContext. This is the main interface you have to Core Data; we’ll talk about how to set this up in the next episode.
Make sure the joke doesn’t have an error id with a guard statement, and if it doesn’t, make an NSFetchRequest that compares the id of this joke with the id values in the database. This allows you to check if the joke already exists as a JokeManagedObject in the database, and if so update its fields accordingly.
If no results come back, make a new JokeManagedObject, and set the fields accordingly. Once that is done, call save on the viewContext - this pushes the changes in the context down to the database.
Deleting is much easier than saving. Since we have a collection of Jokes (and therefore a collection of JokeManagedObjects in Core Data), we can extend the Collection type like this:
extension Collection where Element == JokeManagedObject, Index == Int {
// 1
func delete(at indices: IndexSet, inViewContext viewContext: NSManagedObjectContext) {
// 2
indices.forEach { index in
viewContext.delete(self[index])
}
// 3
do {
try viewContext.save()
} catch {
fatalError("\(#file), \(#function), \(error.localizedDescription)")
}
}
}
Limit the extension to cases where Element == JokeManagedObject; this will restrict the delete method appropriately. Inside the delete method, for each index in the IndexSet, ask the viewContext to delete the element at that index of the collection. Again, at the end of the method, call save on the context to push the changes down into the database.
This gives us the methods we need to save and delete, but we need to give our app a hook into Core Data, and we’ll do that in the next episode.