NSFetchedResultsControllerDelegate not called when data is not matching predicate anymore - core-data

The problem I am trying to solve is the following: a predicate of an NSFetchedResultsController is restricting the results to specific conditions. But when data objects either newly satisfy or dissatisfy conditions, the NSFetchedResultsControllerDelegate is not getting called.
My NSFetchedResultsController:
private var _fetchedResultsController : NSFetchedResultsController<CardIndex>? = nil
var fetchedResultController : NSFetchedResultsController<CardIndex> {
get {
if _fetchedResultsController == nil {
let fetchRequest: NSFetchRequest<CardIndex> = CardIndex.fetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor.init(key: "index", ascending: true)]
fetchRequest.predicate = NSPredicate.init(format: "consumer == %# AND card.definition != nil AND card.stateInt == 0", consumer)
_fetchedResultsController = NSFetchedResultsController<CardIndex>.init(fetchRequest: fetchRequest, managedObjectContext: CDManager.shared.one.viewContext, sectionNameKeyPath: nil, cacheName: nil)
do {
try _fetchedResultsController?.performFetch()
} catch {
Log(.Warning, "...")
}
_fetchedResultsController?.delegate = self
}
return _fetchedResultsController!
}
}
The data structure that is relevant here (only pseudo code):
class CardIndex {
#NSManaged public var index: Int16
#NSManaged public var consumer: String?
#NSManaged public var card: CardApplication?
}
class CardApplication {
#NSManaged public var title: String?
#NSManaged public var indexes: NSSet?
#NSManaged public var stateInt: NSNumber?
#NSManaged public var definition: CardDefinition?
}
When the NSFetchedResultsController is initially fetching the data, it correctly returns all CardApplications that have a definition set and the stateInt has the value 0. But when I change the value of stateInt during runtime to another value that is not matching the predicate condition anymore, the NSFetchedResultsControllerDelegate is not being called reflecting that change in the data. I made sure that I save the managed object's context after changing the stateInt value and I can also see that the context hasChanges before saving and the CardApplication managed object is also showing the change on property changedValues(). From debugger console before saving the change, where self is the CardApplication object I changed stateInt of:
(lldb) po managedObjectContext!.hasChanges
true
(lldb) po self.isUpdated
true
(lldb) po self.changedValues()
▿ 1 element
▿ 0 : 2 elements
- key : "stateInt"
- value : 1
Why is the delegate not being called?

As your FRC is configured to fetch CardIndex objects, it only observes changes to those objects. It will consequently not respond to changes to the related CardApplication objects. You could either deliberately “dirty” the CardIndex, which will trigger the FRC to re-evaluate the predicate, or abandon the FRC and use your own observer to update your UI.

Related

Crash when delete a record from coredata in SwiftUI

I make a list for audio items from coredata. after deleting, crash reported as "EXC_BREAKPOINT (code=1, subcode=0x1b8fb693c)", why?
When using
ForEach(items, id: \.self)
, it works. But My Audio has id property and follow Identifiable protocol.
UPDATE: I found adding a if{} clause will fix crash, but why? Breakpoint at "static UUID.unconditionallyBridgeFromObjectiveC(:) ()".
struct Test1View: View {
#Environment(\.managedObjectContext) var context
#FetchRequest(fetchRequest: Audio.fetchAllAudios()) var items: FetchedResults<Audio>
var body: some View {
List {
ForEach(items) { item in
if true { // <- this if clause fix crash, but why?
HStack {
Text("\(item.name)")
}
}
}.onDelete(perform: { indexSet in
let index = indexSet.first!
let item = self.items[index]
self.context.delete(item)
try? self.context.save()
})
}
}
}
code as following:
class Audio: NSManagedObject, Identifiable {
#NSManaged public var id: UUID
#NSManaged public var name: String
#NSManaged public var createAt: Date
}
struct Test1View: View {
#Environment(\.managedObjectContext) var context
var fetchRequest: FetchRequest<Audio> = FetchRequest<Audio>(entity: Audio.entity(), sortDescriptors: [NSSortDescriptor(key: "createAt", ascending: false)])
var items: FetchedResults<Audio> { fetchRequest.wrappedValue }
var body: some View {
List {
ForEach(items) { item in
HStack {
Text("\(item.name)")
}
}.onDelete(perform: { indexSet in
let index = indexSet.first!
let item = self.items[index]
self.context.delete(item)
try? self.context.save()
})
}
}
}
I had the same problem as you.
Probably it is a bad idea to use your own id property to make it Identifiable because Core Data is setting all the properties to nil when the object is deleted, even if you declared it differently in your Swift CoreData class.
When deleting your entity, the id property gets invalidated and the objects .isFault property is set to true, but the SwiftUI ForEach still holds some reference to this ID object (=your UUID) to be able to calculate the "before" and "after" state of the list and somehow tries to access it, leading to the crash.
Therefore the following recommendations:
Protect the detail view (in the ForEach loop by checking isFault:
if entity.isFault {
EmptyView()
}
else {
// your regular view body
}
Expect your id property to be nil, either by defining it accordingly in your core data model as optional
#NSManaged public var id: UUID?
or by not relying on the Identifiable protocol in the SwiftUI ForEach loop:
ForEach(entities, id: \.self) { entity in ... }
or
ForEach(entities, id: \.objectID) { entity in ... }
Conclusion: you really do not need to make all your CoreData properties Swift Optionals. It's simply important that your id property referenced in the ForEach loop handles the deletion (=setting its value to nil) gracefully.
I found the reason of crash, must provide optional, because of OC/swift object conversion:
convert
class Audio: NSManagedObject, Identifiable {
#NSManaged public var id: UUID
#NSManaged public var name: String
#NSManaged public var createAt: Date
}
to
class Audio: NSManagedObject, Identifiable {
#NSManaged public var id: UUID?
#NSManaged public var name: String?
#NSManaged public var createAt: Date?
}
I had the same issue over the weekend. It looks like SwiftUI want's to unwrap the value i read from CoreData and as the value is already deleted it crashes.
In my case i did solve it with nil coalescing on all values i use from CoreData.
You can try to provide a default value on your item.name with
ForEach(items) { item in
HStack {
Text("\(item.name ?? "")")
}
}

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)

Setting Bool attribute of an NSManagedObject throws "Uncaught exception in iOS 10"

I am upgrading my app to iOS 10 / swift 3
So constructing an NSManagedObject with a dictionary (values received from remote db) this way...
let writerDictionary = [
...: writer.id,
...: writer.name,
...: writer.picture,
...: writer.publicationID,
...: writer.language,
...: writer.country
]
let newWriter = Writer(dictionary: writerDictionary, context: SHARED_CONTEXT)
isFavorite is an NSManaged property that I set manually to true with newWriter.isFavorite = true right after constructing the newWriter object before saving the context -
This line crashes with Uncaught Exception in iOS 10 - Previously there was no problem whatsoever with iOS 9
I have also tried newWriter.setValue(true, forKey: "isFavorite") - While it does not crash the app, the bool value isFavorite remains false unchanged.
Any ideas? This is my Writer subclass of NSManagedObject
import UIKit
import CoreData
#objc(Writer)
class Writer: NSManagedObject {
// Attributes also in the database
#NSManaged var id: NSNumber
#NSManaged var name: String
#NSManaged var picture: String?
#NSManaged var publicationID: NSNumber
#NSManaged var language: String
#NSManaged var country: String
#NSManaged var lastArticle: Date
// Attributes only in the iOS app
#NSManaged var isFavorite: Bool // Initial value is false
#NSManaged var hasNewArticles: Bool
// Relationship objects
#NSManaged var publication: Publication
#NSManaged var articles: [Article]
var writerImage: UIImage? {
get {
return ImageCache.sharedCache.imageWithIdentifier("writers-" + String(id))
}
set {
ImageCache.sharedCache.storeImage(newValue, withIdentifier: "writers-" + String(id))
}
}
override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
super.init(entity: entity, insertInto: context)
}
init(dictionary: [String : AnyObject], context: NSManagedObjectContext) {
let entity = NSEntityDescription.entity(forEntityName: "Writer", in: context)!
super.init(entity: entity, insertInto: context)
id = ...
name = ...
picture = ...
publicationID = ...
language = ...
country = ...
lastArticle = ...
}
override func prepareForDeletion() {
if let _ = writerImage {
writerImage = nil
}
}
}
Solution
The current version of Swift 3 beta seems to have some flaw about treating properties prefixed with "is".
Adjusting the name of the property like this, solved it...
#NSManaged #objc(isFavorite) var isFavorite: Bool
Fixed in Xcode 8 beta 3 compiler.
From the release notes:
Earlier Swift 3 betas implicitly renamed the property, getter, and
setter for #objc properties of type Bool whose names started with
“is”, to match the Objective-C naming conventions. (For example, a
Swift boolean property named isMagical generated an Objective-C
declaration of the form #property (nonatomic, getter=isMagical) BOOL
magical.) This turned out to cause more trouble than it was worth and
has been removed. (26847223)

Resources