How can I use NSPredicate with a computed property in Swift? - core-data

I am trying to create a #FetchRequest in SwiftUI using an NSPredicate, like so:
#FetchRequest(entity: CoreDataRecipe.entity(), sortDescriptors: [], predicate: NSPredicate(format: "isRunning == true")) var runningRecipes: FetchedResults<CoreDataRecipe>
Unfortunately, my isRunning property is a computed property like so:
var isRunning: Bool {
get {
activeStep != nil
}
}
The problem is that NSPredicate will crash since it can't find a string for the computed property when resolving the predicate. Is there any way to make this possible? I would love to use this, since it would allow me to create a nice UX.
Note: I can't test for activeStep as an alternative, because it is a
computed property as well with more complex logic.

Related

Is it possible to save a value to CoreData using key path?

In a parent view, I have this:
LongPressEditableText(contents: "\(workout.name ?? "")", context: workout, keyPath: \WorkoutEntity.name)
referencing a string field of a WorkoutEntity in CoreData.
The LongPressEditableText is to be a component which is usually just a Text(), but when long pressed, becomes a TextField with the same contents, editable. On submit it should update the UI (it does this fine), but it should also save the new value to the appropriate spot in CoreData.
struct LongPressEditableText: View {
#State var contents: String
#Environment(\.managedObjectContext) private var viewContext
var context: NSObject
var keyPath: KeyPath<NSObject, String?>
#State var inEditMode: Bool = false
var body: some View {
if inEditMode {
TextField("test", text: $contents)
.onSubmit {
context[keyPath: keyPath] = contents
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
inEditMode.toggle()
}
} else {
Text(contents)
.onLongPressGesture {
inEditMode.toggle()
}
}
}
}
At the moment, I get two errors. In my parent view Cannot convert value of type 'KeyPath<WorkoutEntity, String?>' to expected argument type 'KeyPath<NSObject, String?>' and in the LongPressEditableText view Cannot assign through subscript: key path is read-only
I can solve the first by forcing KeyPath but that's not a solution as I want the editable field to work with a number of different entities with string fields, so I'd like it to be generic. The second I am stumped about, this is as close as I've been able to get to success.
"Generics isn’t my primary concern...", yes it is because it is a very helpful solution here that tells the compiler and runtime what type of object is used in the text field.
First of all since this is Core Data we shouldn't use NSObject but instead NSManagedObject so lets make the view generic with a type that inherits from NSManagedObject and then use the generic type inside for the properties.
struct LongPressEditableText<ManagedObject: NSManagedObject>: View {
#Environment(\.managedObjectContext) private var viewContext
#State private var contents: String = ""
#State var object: ManagedObject
var keyPath: ReferenceWritableKeyPath <ManagedObject, String?>
Notice that the property object (context in your code) is declared to be of the generic type and that the keyPath is also defined to hold the same type. I have also changed from KeyPath to ReferenceWritableKeyPath since the generic type is a class and we want to use the key path to update the object.
And to use the field here is an example, since the view is generic the compiler can deduct that the generic type is Item and also check that it has a property text
struct DetailView: View {
#ObservedObject var item: Item
var body: some View {
VStack {
LongPressEditableText(object: item, keyPath: \.text)
}
.padding()
}
}

How can I run a simple function on Core Data results

Apologies if this is a very basic question, I'm just learning how to use Core Data.
I'm trying to run a simple function (Text("\(items.timestamp.count)")) on the data Apple gives you by default (a list of items with just one attribute: timestamp). However, I keep getting the error "Value of type 'FetchedResults' has no member 'timestamp".
Can anyone explain why this happens? When the timestamp attribute is accessed within the ForEach loop automatically provided by Apple, everything seems to works fine.
import SwiftUI
import CoreData
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
var body: some View {
Text("\(items.timestamp.count)")
List {
ForEach(items) { item in
Text("Item at \(item.timestamp!, formatter: itemFormatter)")
}
}
}
}
private let itemFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .medium
return formatter
}()
it's because items (aka FetchedResults) is an array of item. So it can contain multiple returned Items.
The ForEach is working because a ForEach by nature iterates through the array and displays each one.
if you'd like to just display the count of items then remove the "timestamp" part:
Text("\(items.count)")
if you'd like to display the timestamp then you'd need to specify which item in the array you want to display the timestamp for. so to display the timestamp for the first item in the array then you can do:
Text("\(items[0].timestamp ?? Date())")
(note that if there are no items in the database then you will get today's date. Also it's more common to use a list and ForEach which will display all records in a list including timestamps (like you have in the rest of Apple's boilerplate code))

How to reload a row of SwiftUI Core Data-backed list if object properties change?

I have a standard SwiftUI list setup, powered by Core Data FetchRequest.
struct SomeView: View {
var container: Container
var myObjects: FetchRequest<MyObject>
init(container: Container) {
let predicate : NSPredicate = NSPredicate(format: "container = %#", container)
self.container = container
self.myObjects = FetchRequest<MyObject>(entity: MyObject.entity(), sortDescriptors: [NSSortDescriptor(key: "date", ascending: true)], predicate: predicate)
}
var body: some View {
VStack(spacing: 0.0) {
List(myObjects.wrappedValue, id: \.uniqueIdentifier) { myObject in
rowView(for: myObject, from: self.myObjects.wrappedValue)
}
}
}
}
Everything works well when items are added and deleted. RowView returns a view that presents different content based on various properties of myObject.
Problem: when I modify a particular myObject elsewhere in the app (change one of its properties), and save the associated Core Data ManagedObjectContext, the List row representing that item is not updated/refreshed in the UI.
Possibly a cause for this is that I am updating my Core Data object by setting a property, that in turn sets another property. Maybe the associated signaling doesn’t reach the right place, and I should emit more notifications here.
Code in MyObject. ObjectType is an enum, typeValue is int32 backing this, that actually gets stored in CD database.
var type: ObjectType {
get {
return ObjectType(rawValue: typeValue)!
}
set {
self.typeValue = newValue.rawValue
}
}
How do I cause a list row to update when the backing Core Data object is modified and saved elsewhere in the app?
I finally figured this out on my own. The fix was not in the list, but further down the stack, in RowView.
RowView code was such:
struct RowView: View {
var myObject: MyObject
// Other code to render body etc
}
When doing this, the RowView works as expected, but it treats myObject as immutable. Any changes to myObject don’t cause a view redraw.
The one-keyword fix is to add #ObservedObject to the declaration:
struct RowView: View {
#ObservedObject var myObject: MyObject
}
It now works as expected, and any updates to MyObject cause a redraw.

Input a dynamic value into #FetchRequest, to fetch a single entity from core data in SwiftUI

I saw same type of error but with different kind of code here, so I think it's better to ask a new question on this context. I have attempted to "find a specific entity" from core data by trying to pass a string variable (which use as a key to find that entity) called title into #FetchRequest. This is part of the code I have used
struct AccountMainPage: View {
//*** User input ***
var title: String
//*** Core data enviroment initialisation ***
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(fetchRequest: Accounts.getSpecificAccounts(findTitle: title)) var fetchedAccount: FetchedResults<Accounts>
var body: some View {
//SOME CODE HERE
}
}
The public class Accounts has the extension:
extension Accounts {
static func getSpecificAccounts(findTitle: String) -> NSFetchRequest<Accounts> {
let request: NSFetchRequest<Accounts> = Accounts.fetchRequest() as! NSFetchRequest<Accounts>
let findDescriptor = NSPredicate(format: "title == %#",findTitle)
request.predicate = findDescriptor
return request
}
}
However, the line with #FetchRequest(fetchRequest: Accounts.getSpecificAccounts(findTitle: title)) var fetchedAccount: FetchedResults<Accounts> has a syntax error:
Cannot use instance member 'title' within property initializer; property initializers run before 'self' is available
Is there something wrong with my code?
#FetchRequest is dynamic property which is initialised, as any other property, before your AccountMainPage init is called, so self is not available yet, that is why you cannot use title property which is a member of self, and that is about what compiler error tells.
So here is a possible solution: we initialise fetch request property with stub request and then in init, which is called later, reinitialise it with real fetch request.
Here is an approach demo (all unrelated things cut):
struct ContentView: View {
var title: String
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(fetchRequest: Accounts.fetchRequest()) var fetchedAccount: FetchedResults<Accounts>
init(title: String) {
self.title = title
_fetchedAccount = FetchRequest<Accounts>(fetchRequest: Accounts.getSpecificAccounts(findTitle: title))
}
...

SwiftUI & Core Data - How do I use a parent record in a predicate?

Is it possible to use a core data record in a predicate inside the #FetchRequest property wrapper in SwiftUI?
I have a list of Project and a list of Tasks. I want to tap on a project and navigate to a list of related tasks for that project. I can't seem to find a way to pass in the parent project in a way that SwiftUI can see before the #FetcheRequest is initialized.
I tried placing the parent project in an EnvironmentObject. This is called when I navigate from the ProjectListView to the TaskListView.
TaskListView()
.environment(\.managedObjectContext, self.managedObjectContext)
.environmentObject(self.projectToEdit)
Then in the TaskListView I added tried this:
#Environment(\.managedObjectContext) var managedObjectContext
#EnvironmentObject var parentProject: Project
#FetchRequest(
entity: Task.entity(),
sortDescriptors: [
NSSortDescriptor(keyPath: \Task.name, ascending: true)
],
predicate: NSPredicate(format: String(format: "%#%#", "taskProject", " == %#"), parentProject)
) var tasks: FetchedResults<Task>
I get the following error on the line with the predicate.
Cannot use instance member 'parentProject' within property initializer; property initializers run before 'self' is available
So is there a way to write a predicate in some way that can use the parent project? Passing the project to the task view does not seem like it's going to work. How else would I go about using a record in a predicate like this?
The FetchRequest can be dynamically created in the init method. That way you can vary predicate and sort conditions. Here is some sample code to achieve that.
// sample Project class
class Project:NSManagedObject {
var id : String
var name : String
}
// sample Task class
class Task:NSManagedObject {
var id : String
var prjId : String
var name : String
}
// Task List View
struct TaskListView: View {
#Environment(\.managedObjectContext) var managedObjectContext
private var tasksRequest: FetchRequest<Task>
private var tasks: FetchedResults<Task> { tasksRequest.wrappedValue }
private var project:Project
// init Task with Project
init(_ project:Project) {
self.project = project
// create FetchRequest
self.tasksRequest = FetchRequest(
entity: Task.entity(),
sortDescriptors: [NSSortDescriptor(key: "name", ascending:true)],
predicate: NSPredicate(format: "prjId == %#", project.id))
}
var body: some View {
VStack {
Section(header: Text("Tasks under \(project.name):")) {
// access the fetched objects
ForEach(tasks, id:\.id) { task in
Text("\(task.name)")
}
}
}
}
}
Then the call to TaskListView() would look like:
// call to TaskListView
TaskListView(self.projectToEdit)
.environment(\.managedObjectContext, self.managedObjectContext)

Resources