At this point, we’ve written helper methods to save to and delete from the Core Data database. But once we have the data there, we need a way to fetch it from the database and keep the UI up to date.
Luckily, in iOS 13 a property wrapper was introduced that can help with that - @FetchRequest. It’s uses a generic Result, where the Result inherits from NSFetchRequestResult
// On slide: the signature for @FetchRequest
Let’s look at the different initializers that it has:
This initializer looks very similar if you’ve used Core Data in the past, since it mirrors NSFetchRequest. The argument list consists of an NSEntityDescription, which is the entity you are trying to fetch from the database, an array of NSSortDescriptors, which describe how to sort the returned data, and 2 optional arguments: an NSPredicate, which can help filter the set of fetched results, and an Animation, which governs the animation used for any changes to the fetched results, which can come into play when keeping a SwiftUI list up to date, for example.
//On slide:
init(entity: NSEntityDescription, sortDescriptors: [NSSortDescriptor], predicate: NSPredicate?, animation: Animation?)
This initializer is available when the Result inherits from NSManagedObject, and is similar to the last one, except there is no NSEntityDescription. This is inferred using Result.entity()
//On slide:
init(sortDescriptors: [NSSortDescriptor], predicate: NSPredicate?, animation: Animation?)
The last two initializers take in a normal NSFetchRequest, as well as either an Animation, or a Transaction, both of which are used when the fetched results change.
//On slide:
init(fetchRequest: NSFetchRequest<Result>, animation: Animation?)
//On slide:
init(fetchRequest: NSFetchRequest<Result>, transaction: Transaction)
With that knowledge in place, let’s look at how to save, fetch and delete jokes from the database in a demo.
Having placed the managedObjectContext in the SwiftUI Environment in the last episode, it’s easy to grab that and use it in any view, so add it to the Views/JokeView.swift file before the viewModel is declared:
@Environment(\.managedObjectContext) private var viewContext
Now, go down to the handle method, and under the default case, add a check for decisionState == .liked; this will capture the case when the user likes the joke, and therefore wants it saved for helping them become the life of the party in the future:
if decisionState == .liked {
JokeManagedObject.save(joke: viewModel.joke,
inViewContext: viewContext)
}
Thanks to the helper method defined earlier, it’s easy to save the joke, now since we have the viewContext in hand.
Before we leave this file, we need to pass the viewContext into the SavedJokesView so that it has access. This can be done by passing the viewContext into the environment when the SavedJokesView is made, up above in the sheet modifier.
This can be done by attaching a sheet modifier to the NavigationView here.
.sheet(isPresented: $presentSavedJokes) {
SavedJokesView()
.environment(\.managedObjectContext, self.viewContext)
}
Now with saved jokes in the database, you can populate a SwiftUI view with those jokes. Go to Views/SavedJokesView.swift, and again, you’ll need a reference to the viewContext that you have stored in the SwiftUI Environment:
@Environment(\.managedObjectContext) private var viewContext
The current code declares an array of Strings to represent the jokes. Replace that with a @FetchRequest:
@FetchRequest(
sortDescriptors: [NSSortDescriptor(
keyPath: \JokeManagedObject.value,
ascending: true
)],
animation: .default
) private var jokes: FetchedResults<JokeManagedObject>
If you are familiar with NSFetchRequest at all, this property wrapper works in a similar fashion. It will - using this version of the initializer - grab all JokeManagedObjects from the database, sort them using the value property of the JokeManagedObject, and animate them into the list that shows them with the default animation. This property wrapper also sets up a publisher-subscriber relationship - as changes to the database take place, the fetch is automatically repeated and the UI is updated, since the underlying state has changed. This is very similar to the normal @State/@Binding behavior in SwiftUI, with the addition of the fetch request to handle the updating of the data.
To handle deletion of jokes, go to the ForEach block of code, and update the .onDelete modifier to use the delete method we defined in the earlier episode. Recall that this takes in an IndexSet so it works perfectly with the List that comes out of ForEach
ForEach(jokes, id: \.self) { joke in
Text(self.showTranslation ? joke.translatedValue ?? "N/A"
: joke.value ?? "N/A")
.lineLimit(nil)
}
.onDelete { indices in
// 2
self.jokes.delete(at: indices,
inViewContext: self.viewContext)
}
Now let’s run this in the simulator. We can add jokes to the database by swiping right, look at our list of saved jokes which uses the @FetchRequest, and delete jokes from the list as well.
That adds the last bit of features for our app - but we really need some units tests, which we’ll handle next.