We’ve arrived at day 58 of the 100 Days of SwiftUI! We’re continuing our deep dive into Core Data for the next few days. Yesterday, we learned how \.self works, how to save a managed object context only when needed. Today, we’ll learn about NSPredicate, changing fetch requests dynamically, creating relationships, and more. Let’s dive in!
Filtering @FetchRequest using NSPredicate
When we use the @FetchRequest
property wrapper, we can provide an array
of SortDescriptors
to tell SwiftUI how to order the results. We can also provide an NSPredicate
object, which is a test to apply to those results. Only objects that pass that test will be in the final array
.
//
// ContentView.swift
// CoreDataProject
import CoreData
import SwiftUI
struct ContentView: View {
@Environment(\.managedObjectContext) var moc
@FetchRequest(sortDescriptors: [], predicate: NSPredicate(format: "universe == %@", "Star Wars")) var ships: FetchedResults<Ship> // Filters ships where the universe is Star Wars.
var body: some View {
VStack {
List(ships, id: \.self) { ship in
Text(ship.name ?? "Unknown name")
}
Button("Add Examples") {
let ship1 = Ship(context: moc)
ship1.name = "Enterprise"
ship1.universe = "Star Trek"
let ship2 = Ship(context: moc)
ship2.name = "Defiant"
ship2.universe = "Star Trek"
let ship3 = Ship(context: moc)
ship3.name = "Millenium Falcon"
ship3.universe = "Star Wars"
let ship4 = Ship(context: moc)
ship4.name = "Death Star"
ship4.universe = "Star Wars"
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
There are a lot of ways to use NSPredicate
, so I would encourage you to check out Paul’s article on them.
Dynamically filtering @FetchRequest with SwiftUI
One of the SwiftUI questions I’ve been asked more than any other is this: how can I dynamically change a Core Data
@FetchRequest
to use a different predicate or sort order? The question arises because fetch requests are created as a property, so if you try to make them reference another property Swift will refuse.There is a simple solution here, and it is usually pretty obvious in retrospect because it’s exactly how everything else works: we should carve off the functionality we want into a separate view, then inject values into it.
Hacking with Swift, Paul Hudson (@twostraws)
//
// ContentView.swift
// CoreDataProject
import CoreData
import SwiftUI
struct ContentView: View {
@Environment(\.managedObjectContext) var moc
@State private var lastNameFilter = "A"
var body: some View {
VStack {
FilteredList(filterKey: "lastName", filterValue: lastNameFilter) { (singer: Singer) in
Text("\(singer.wrappedFirstName) \(singer.wrappedLastName)")
}
Button("Add Examples") {
let taylor = Singer(context: moc)
taylor.firstName = "Taylor"
taylor.lastName = "Swift"
let ed = Singer(context: moc)
ed.firstName = "Ed"
ed.lastName = "Sheeran"
let adele = Singer(context: moc)
adele.firstName = "Adele"
adele.lastName = "Adkins"
try? moc.save()
}
Button("Show A") {
lastNameFilter = "A"
}
Button("Show S") {
lastNameFilter = "S"
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
//
// 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
}
}
}
//
// FilteredList.swift
// CoreDataProject
import CoreData
import SwiftUI
struct FilteredList<T: NSManagedObject, Content: View>: View {
@FetchRequest var fetchRequest: FetchedResults<T>
let content: (T) -> Content
var body: some View {
List(fetchRequest, id: \.self) { item in
self.content(item)
}
}
init(filterKey: String, filterValue: String, @ViewBuilder content: @escaping (T) -> Content) {
_fetchRequest = FetchRequest<T>(sortDescriptors: [], predicate: NSPredicate(format: "%K BEGINSWITH %@", filterKey, filterValue))
self.content = content
}
}
Because this was quite complex and at times a bit hard to follow, I’ve not made any comments in the code. They would’ve just been way too long. Instead, I would implore you to watch Paul’s video and read the article to ensure you have a decent grasp on what’s going on. Click here to go to it.
One-to-many relationships with Core Data, SwiftUI, and @FetchRequest
Core Data allows us to link entities together using relationships. When we’ll request an object, we’ll get those relationships provided to us automatically. This isn’t quite easy, because this is where Core Data shows it’s age, according to Paul.
Again, I would urge you to go through the video yourself to get a good grasp of what’s going on.
//
// ContentView.swift
// CoreDataProject
import CoreData
import SwiftUI
struct ContentView: View {
@Environment(\.managedObjectContext) var moc
@FetchRequest(sortDescriptors: []) var countries: FetchedResults<Country>
var body: some View {
VStack {
List(countries, id: \.self) { country in
Section(country.wrappedFullName) {
ForEach(country.candyArray, id: \.self) { candy in
Text(candy.wrappedName)
}
}
}
}
Button("Add Examples") {
let candy1 = Candy(context: moc)
candy1.name = "Mars"
candy1.origin = Country (context: moc)
candy1.origin?.shortName = "UK"
candy1.origin?.fullName = "United Kingdom"
let candy2 = Candy(context: moc)
candy2.name = "KitKat"
candy2.origin = Country (context: moc)
candy2.origin?.shortName = "UK"
candy2.origin?.fullName = "United Kingdom"
let candy3 = Candy(context: moc)
candy3.name = "Twix"
candy3.origin = Country (context: moc)
candy3.origin?.shortName = "UK"
candy3.origin?.fullName = "United Kingdom"
let candy4 = Candy(context: moc)
candy4.name = "Toblerone"
candy4.origin = Country (context: moc)
candy4.origin?.shortName = "CH"
candy4.origin?.fullName = "Switzerland"
try? moc.save()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Wrap up
And that’s it for day 58! It was quite a long day, but for the time we invested, we learned valuable features of Core Data that will help us create more robust and advanced apps in the future. We’re not quite done with Core Data yet though, so it’s time to recharge and look forward to tomorrow. See you then!
100 Days of SwiftUI – Day 58