I have a NSFetchedResultController with this predicate set:
NSPredicate* pred = [NSPredicate predicateWithFormat:#"author != %# && deleted != %#", [NSNumber numberWithLongLong:0],[NSNumber numberWithBool:YES]];
It filters fine on startup, and I get a delegate callback and list updates fine if objects are added or deleted.
But if I change the "deleted" field, the NSFetchedResultController set is NOT updated, nor do I get the callback.
Though, the actual object in the NSFetchedResultController is updated, if I do a "reloadData" on my table and check the value of "deleted", it is actually set to YES.
Why is it not disappearing from the NSFetchedResultController?
Is this expected behavior?
Or what could I be doing wrong?
I guess I solved it...
Turns out I actually do get the callback ("didChangeObject") and I now check the value of "deleted" here and manually remove it from the NSFetchedResultController if the value is YES.
Seems to work, but I still did expect it to be automatic.
Related
I have a simple Core Data entity Story that occasionally I update with the latest data from a network call. This network call sometimes updates many, many stories instances, so I run an NSBatchInsertRequest, shown below. (The other reason I'm using a batch insert is that many stories might need to be added to the persistent store.)
The problem is a user can have already marked a Story as a favorite. When they do that, I set story.isFavorite = true on the main thread and save viewContext.
However, when the batch insert occurs it overwrites story.isFavorite, setting it back to false, even though I'm using NSMergeByPropertyObjectTrumpMergePolicy on both the batch insert and view contexts. I am not touching story.isFavorite in the batch insert handler either so I don't expect that property to be overwritten.
I thought the benefit of a batch insert with this merge policy was to avoid first fetching + then manually updating changed properties + finally saving. What is the right way to avoid changing property values in an NSBatchInsertRequest?
Story
#objc(Story)
public class Story: NSManagedObject {
#NSManaged public var title: String?
#NSManaged public var storyURL: URL?
#NSManaged public var updatedTime: Date?
#NSManaged public var isFavorite: Bool // <- the problem property
}
Batch insert
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
container.viewContext.automaticallyMergesChangesFromParent = false
let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
context.parent = container.viewContext
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
context.perform {
let batchInsert = NSBatchInsertRequest(entity: Story.entity(), managedObjectHandler: { managedObject in
let story = managedObject as! Story
let storyResponse = downloadedStories[I]
// Update story with latest response data BUT don't modify story.isFavorite.
story.title = storyResponse.title
story.storyURL = storyResponse.storyURL
story.updatedTime = storyResponse.updatedTime
// ...
})
let result = try context.execute(batchInsert) as? NSBatchInsertResult
if let insertedIDs = result?.result as? [NSManagedObjectID] {
// Merge changes into parent context. Skip save() because not needed for batch insert.
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: [NSInsertedObjectsKey: insertedIDs], into: [container.viewContext])
}
}
Edit
The Story entity does have a unique value constraint using attribute storyURL.
Update after Michael Tsai's answer
By making the Story entity attribute isFavorite a non-Optional Boolean without a default value (it was marked as Optional before, though I'm not sure it makes a difference here) and keeping the Use Scalar Type box checked, I can confirm that existing objects in the store will not be modified (at all) with this configuration of the batch insert context.
context.persistentStoreCoordinator = container.persistentStoreCoordinator
// HOWEVER, observe that regardless of the merge policy below,
// setting `context.parent = container.viewContext` will also
// overwrite the store data!
context.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy
// NSMergeByPropertyObjectTrumpMergePolicy ignores objects in the store
// (which have the same unique constraint value, here equal `storyURL`)
// and overwrites all properties.
// To confirm that the batch insert operation does not modify
// existing Story instances (at all), first delete all instances where
// where isFavorite == false. Then load the all story data again and
// execute the NSBatchInsertRequest with this change to managedObjectHandler:
story.title = storyResponse.title + " (modified)"
You will see the missing stories get inserted back, this time with their titles having a suffix " (modified)"; but previously favorited stories
do not get modified (basically, with this setup, the batch insert won't re-insert objects).
So the isFavorite property does not get overwritten BUT neither do any properties that should be changed (because they received a new title, for example).
Therefore, if you don't want your objects to get updated, but you want completely new objects to be inserted, you can use this approach.
However, if you are expecting your objects to require updates here are some alternatives:
you may opt to run a separate update operation, maybe an NSBatchUpdateRequest after you run your batch insert in this way,
or after the batch insert, you can update certain properties in a simple loop in a (possibly background/child) context without a batch operation, which could be fine if there isn't tons of data;
lastly, you might be able to first batch insert new data to a temporary store before somehow manually merging your choice of properties with the new store, then delete the temporary store.
A simpler approach: you could fetch the all properties you want to keep unchanged before you execute the batch insert (storing them in an dictionary keyed by your object's uniqueness constraint value), and then during the batch insert set the property again.
For this approach, you will want to use a different merge policy such as NSMergeByPropertyObjectTrumpMergePolicy so that the updated object gets re-inserted into the store (make sure to fetch all properties that you don't want to lose in advance of the batch insert)
random idea: How to Save Data When Using One ManagedObjectContext and PersistentStoreCoordinator with Two Stores
I don't think it is actually possible to do a partial update with a batch insert request. It's hard to know for sure because I don't think any of this is documented except in WWDC sessions. When I first watched the 2019 session, I was excited because the presenter said:
Attributes that are optional or configured with default values can be omitted from the dictionary as well.
In the case of updating an object with unique constraint, the existing values will not be changed.
I took this to mean that:
You can omit values for new objects, and you'll get the defaults or NULL. That makes sense.
If there's an existing object and you omit a value, that value will not the changed. So you can purposely omit values to do a partial update, i.e. update other values while leaving your isFavorite alone.
But, after writing code to test this and looking at the output from com.apple.CoreData.SQLDebug, what actually seems to happen with NSMergeByPropertyObjectTrumpMergePolicy is:
If you omit a value that's required you get a validation error.
If you omit a value that's optional, it updates the row to NULL. For a Bool property in Swift, this will become false.
If you omit a value with a default value, it updates the row to the default.
This is a shame because it seems like partial updates could be implemented by having the ON CONFLICT clause only specify DO UPDATE SET for the attributes that you actually set. But (as of macOS 11) Core Data seems to always generate SQL to set all of the columns.
In summary, with batch inserts, NSMergeByPropertyObjectTrumpMergePolicy does not actually merge by property based on what's changed (like with a regular Core Data save). Rather, it either inserts a new row (if the object is absent) or overwrites all the columns but preserves the objectID (if the object was present).
NSMergeByPropertyStoreTrumpMergePolicy also doesn't merge by property. It just means to leave the stored object alone if it's already present.
Update (2021-06-24): I heard from DTS that Apple considers the current (iOS 14/macOS 11) behavior described above a bug, and that it should let you batch insert without changing omitted properties. The Radar number is 79747419.
I have a SwiftUI app where I am using #FetchRequest along with a predicate. Everything works fine as long as there is already some data.
However, when the app is first installed and the user tries to perform a search before entering any data, the app crashes with this error:
error: SQLCore dispatchRequest: exception handling request: <NSSQLFetchRequestContext: 0x60000188c380> , unimplemented SQL generation for predicate : (username CONTAINS[cd] "r") with userInfo of (null)
I believe the cause of the problem is that the column doesn't exist (because there is no data). What I'd like to do is pass nil for the predicate in the case where this is no data.
I understand how to use NSManagedObjectContext and count(for:) but the context isn't really made available via the #EnvironmentObject at the time I need to use it. Does anyone have any suggestions as to how to handle this. I don't see how a try-catch would work either.
Thanks.
Well, I do have a solution. Its not pretty, but it does work. Because the NSManagedObjectContext is being passed around via #Environment it isn't initialized when a SwiftUI View init is being run. So to get the context you can do:
if let context = (UIApplication.shared.delegate as? AppDelegate)?.persistentContainer.viewContext {
and make the call to get the count in there.
So I'm clueless sometimes. I did not read the error thoroughly since it happened in the context of something I was changing so I thought the problem was that. But no, that is not the problem.
The problem is that earlier in the day, I renamed a column from username to something else. But I forgot to change the NSPredicate.
So for the sake of anyone else using SwiftUI and #FetchRequest, if you do need to count the number of items, you will need to revert to NSFetchRequest to do it. Sorry for wasting anyone's time.
I'm planning to make some of my app content publicly indexable, and for that I am using NSUserActivity. From my experiments so far, I've discovered that apparently the only activity that appears in the search results is the last one to get becomeCurrent called on. Is there a way to make all my activities searchable?
The following code is on my appDelegate:
for (Shop* shop in shopManager)
{
NSUserActivity* activity = [[NSUserActivity alloc] initWithActivityType:ACTIVITY_OPEN_SHOP];
activity.userInfo = #{#"additional1": shop.name};
activity.eligibleForPublicIndexing = YES;
activity.eligibleForSearch = YES;
activity.keywords = shop.indexableKeywords;
CSSearchableItemAttributeSet* attributeSet = [[CSSearchableItemAttributeSet alloc] initWithItemContentType:(NSString*)kUTTypeText];
attributeSet.title = shop.name;
attributeSet.contentDescription = shop.indexableDescription;
attributeSet.keywords = [shop.indexableKeywords allObjects];
[activity setContentAttributeSet:attributeSet];
[activity becomeCurrent];
[activities addObject:activity];
}
self.userActivities = [[NSSet alloc] initWithArray:activities];
Hey I have code that is very similar to yours and spotlight is able to index all of my NSUserActivity objects. My guess is that your NSUserActivity objects go out of reference as soon as the next iteration of the loop occurs. Try adding a strong property.
From this source on Apple Forums:
https://forums.developer.apple.com/message/13640#13640
In my case, I had code that was allocating the NSUA, setting some
properties on it, calling becomeCurrent, but then the object would go
out of scope and deallocated. If you're doing this, try tossing the
activity into a strong property to see if you can then see the results
when you search.
Let me know if it still doesn't work.
RequiredUserInfoKeys is the property of NSUserActivity that you have to set in order to work properly in search results.
activity.requiredUserInfoKeys = [NSSet setWithArray:#[#"additional1"]];
I have met the same problem. I think the reason of this is that previous user activity has not enough time for indexing its metadata by system, the next user activity have became current user activity, so only last one is searchable.
My solution is put latter one into a dispatch_after block and delay 1.5 second, making each of them has time to be indexed.
If someone has a better solution, I would be grateful.
I have a Core Data importer that loops through data, and ignores duplicate records during the import process.
But I have discovered that my NSFetchRequest is not matching against recently stored records that have not been saved. And I am seeing seemingly identical queries delivering different and unexpected results.
For example, in my testing, I discovered that this query matches and returns results:
fetchTest.predicate = [NSPredicate predicateWithFormat:#"%K = 3882", #"intEmployee_id"];
But this seemingly identical one does not:
fetchTest.predicate = [NSPredicate predicateWithFormat:#"%K = %#", #"intEmployee_id", #"3882"];
But - they both match identically after the context is saved to the persistent store.
Apple's documentation says that fetches should work against pending changes by default, and indeed I have conformed that [fetchTest includesPendingChanges] = YES.
Any ideas what on earth is going on here? How is it possible that those two fetches return different results?
Maybe the employee id is not a string but a number? Then the predicate should be:
[NSPredicate predicateWithFormat:#"%K = %#", #"intEmployee_id",
[NSNumber numberWithInt:3882]];
This would imply that the erratic behavior comes from mixing up the types. It still works somehow, even if erratically, because in the SQLite docs it says that SQLite actually does not really distinguish by type when storing the data physically.
See Distinctive Features of SQLite from the SQLite web site, under the heading Manifest Typing.
These actually don't evaluate to the same value.
[NSPredicate predicateWithFormat:#"%K = 3882", #"intEmployee_id"]
evaluates to intEmployee_id = 3882 while
[NSPredicate predicateWithFormat:#"%K = %#", #"intEmployee_id", #"3882"]
evaluates to intEmployee_id = "3882"
Try using a number instead of a string for the id.
I have the following helper method that returns the value from a field.
public static string GetValueFrom(SPListItem item, string fieldName)
{
string value = string.Empty;
if (item.Fields.ContainsField(fieldName))
{
SPField field = item.Fields.GetField(fieldName);
if (item[field.InternalName] != null)
{
value = item[field.InternalName].ToString();
}
}
return value;
}
However for one Field (normal Choice Field) I am getting a ArgumentExecption on this line
if (item[field.InternalName] != null)
I am using
SPListItem item = list.GetItemById(itemId);
To get the item.
I cant find why I am getting the exception when I am checking to see if the field exists?
Any ideas as to why I am getting this Exception for only one field.
Update.
When debugging
The call to GetField() returns the correct field object.
Field.InternalName contains the correct Internal name of the field
If I try and access the value using item["internal name of the field"] it still throws and exception for only this one field.
Sometimes strange things happens and we do not have logical answer to those questions. Try by deleting the list and then creating the list again from scratch. DO NOT try to save it as template and DO NOT try to create the list from that template.
One possible reason of such type of ugly messages is that the security/permissions are not allowing to manipulate that field/column.
Another possible reason of such type of unwanted/unexpected messages is that when the field was created for the first time, its data type was different and later on it was changed to choice. Technically there should be no problem in doing so but sometimes we face odd behavior.
Have you tried debugging? Questions you should answer (because we can't):
Is field a valid value, or null, after the call to GetField()?
If field is not null, what does field.InternalName actually return?
If field.InternalName returns a valid value, can you access it by hard-coding that value in the indexer? i.e. item["fieldInternalName"]
Finding that information may help you solve the problem yourself, but if it doesn't add it to your post so the community has a better chance of helping you.
I do experienced this many a times. The reason for this is if you are logged-in as a non Admin Account(System Account) the default List View Lookup Threshold for the User is 8 for the lookup columns. i.e for the default view the user can access upto the 8 lookup fields only. If you change the List Throttling to >8 it will be resolved. But increasing this will degrade the performance.
Go to Central Admin >> Manage Web Applications >> Select the Web Application >> General Settings Dropdown >> Resource Throttling >> Change the "List View Lookup Threshold" to more than 8
Thanks,
-Codename "Santosh"