No CoreData Fetched Results in Nordic Countries - ios

Frankly, I'm a little embarrassed to even ask this question for the perceived absurdity and without being able to personally reproduce it, but...
In my iOS 7 application, I've received many complaints from users that certain portions of the app "don't work." By "don't work," they mean that a UITableView being populated by a NSFetchRequestDelegate's methods has zero rows. The only thing in common about the error reports are the specific NSFetchRequests that return zero objects and the physical location of the users. The users are all in Nordic countries (Norway, Sweeden, Finland, etc.). This could be a coincidence, or just that the Nordic are more vocal when there is a bug, but this bug makes me nervous.
I've tried setting my phone to use Nynorsk as the default language and using Nordic international units (which my app does happily support with NSDateFormatter and friends). The app works fine for me when I do this, but I am physically in the USA. I've even gone so far as to use a user account with my app whose server-side data was generated in Finland (all sorts of diacritics in strings that will be saved with CoreData), and it still works as expected.
I don't see anything abnormal with my instantiation of my NSFetchedResultsController, but for completeness, here is one that returns no results:
- (NSFetchedResultsController *)fetchedResultsController
{
if (_fetchedResultsController != nil)
return (_fetchedResultsController);
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:kTTAEntityCheckinDetail inManagedObjectContext:MOC];
[fetchRequest setEntity:entity];
[fetchRequest setFetchBatchSize:kTTACheckinDetailBatchSize];
NSSortDescriptor *checkinIDSort = [NSSortDescriptor sortDescriptorWithKey:kTTACheckinDetailAttributeCheckinID ascending:NO];
[fetchRequest setSortDescriptors:#[checkinIDSort]];
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:MOC sectionNameKeyPath:#"sectionIdentifier" cacheName:nil];
_fetchedResultsController = aFetchedResultsController;
NSError *error = nil;
if ([self.fetchedResultsController performFetch:&error] == NO)
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
return (_fetchedResultsController);
}
It's possible that performFetch is failing, but I do not have access to the user's logs. Perhaps I should detect the user's location and log this to a remote server?
Solution: CoreData failed to save the objects because a date field was nil. This field wasn't populated because the locale of a NSDateFormatter was not set. See the comments of #flexaddicted's accepted solution for details.

This could be a shot in the dark.
First, what type of objects do you retrieve? Based on dates?
Then, Yes, I would log both locally and server-side. To log locally you could ask users in Nordic Countries to send a feedback (based on email) with Core Data store or just file logs (the logs will be inserted in the right points). The latter could be achieved by CocoaLumberjack.
An example of this can be found at CocoaLumberjack and Mailing logs by NSScreencast.
TestFlight could be an alternative way but I guess you are in production.

Related

NSFetchedResultsController finds wrong number of objects

I'm seeing a situation where a NSFetchRequest returns a different number of objects depending on whether it's executed directly through the NSManagedObjectContext or as part of building a NSFetchedResultsController.
Sample code:
- (void)setupResultsController {
NSError *error = nil;
NSManagedObjectContext *ctx = [[DataManager sharedInstance] mainObjectContext];
// Create a fetch request and execute it directly
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [Song entityInManagedObjectContext:ctx];
[fetchRequest setEntity:entity];
[fetchRequest setFetchBatchSize:20];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"section" ascending:YES];
NSSortDescriptor *nameDescriptor = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:YES];
NSArray *sortDescriptors = #[sortDescriptor, nameDescriptor];
[fetchRequest setSortDescriptors:sortDescriptors];
NSArray *debugResults = [ctx executeFetchRequest:fetchRequest error:&error];
NSLog(#"Count from context fetch: %lu", (unsigned long)debugResults.count);
// Use the request to populate a NSFetchedResultsController
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc]
initWithFetchRequest:fetchRequest
managedObjectContext:ctx
sectionNameKeyPath:#"section"
cacheName:#"Detail"];
[aFetchedResultsController performFetch:&error];
NSLog(#"Count from results controller fetch: %lu", (unsigned long)[[aFetchedResultsController fetchedObjects] count]);
_songResultsController = aFetchedResultsController;
}
Executing the above results in log messages:
2020-01-10 11:05:07.892772-0500 asb7[12985:105052] Count from context fetch: 10
2020-01-10 11:05:07.893259-0500 asb7[12985:105052] Count from results controller fetch: 9
The difference between the two fetches is that the NSFetchedResultsController is missing the most recently added object. An extreme oddity about this is that, after running the application some seemingly random number of times, the counts start to agree and the new object is fetched.
Edit:
The results become consistent if I pass nil as the cache name or if I remove the second sort descriptor. Obviously these cause undesirable behavior changes but may be clues.
It seems that the NSFetchedResultsController is seeing a stale cache as being valid. Changing a sort descriptor invalidates the cache however updating the persistent store file should invalidate it but apparently does not in this case.
After a bit more experimenting, I have an explanation...if not a solution. Adding new objects does not change the modification date of my .sqlite file. It updates the .sqlite-shm and .sqlite-wal but I'll guess those aren't considered when judging whether to use the cache. Using touch from a terminal session makes the problem go away for the next launch.
(Xcode 10.1, macOS 10.13.6, deployment target 10.3, iOS 12.1 simulator and 10.3.2 device)
Another edit:
I've uploaded a zipped project directory that demonstrates the problem at https://github.com/PhilKMills/CacheTest
What I get is: first run, 3 records for both fetches; second run, 6 and 3. I see it as entirely possible that this depends on my particular software versions but I'm not in a position to upgrade at the moment. Other people's results would be most interesting.
Note: without a FRC delegate being assigned, the problem does not appear.
I suspected that the issue related to the use of the FRC cache, though I was uncertain why that might be. One workaround would therefore be to abandon using the FRC cache. However, that's not ideal.
After further experimentation by the OP, it seems the problem relates to the timestamp on the .sqlite file associated with the persistent store. By inference(*), if the FRC detects that the timestamp has changed, it realises that its cache might be out of date, and rebuilds it - thereby detecting the newly added objects. However, because CoreData by default uses SQLite's "WAL journal mode" (see here and here), database updates are written not to the .sqlite file, but instead to a separate .sqlite-wal file: the timestamp on the .sqlite file is consequently not changed, even though the database has been updated. The FRC continues to use its cache, unaware of the additional objects (or indeed other changes).
A second workaround is therefore to abandon using SQLite's WAL journal mode. This can be achieved by specifying the required journal mode when loading the persistent store, using
NSSQLitePragmasOption:#{#"journal_mode":#"DELETE"}
see here.
(*) In fact, this is documented here:
“the controller tests the cache to determine whether its contents are still valid. The controller compares the current entity name, entity version hash, sort descriptors, and section key-path with those stored in the cache, as well as the modification date of the cached information file and the persistent store file.”

iOS - CoreData deleting objects doesn't free disk space

I am deleting CoreData objects using this method:
NSManagedObjectContext *theContext = [self managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:nameEntity];
[fetchRequest setIncludesPropertyValues:NO]; //only fetch the managedObjectID
[fetchRequest setPredicate:predicate];
NSError *error;
NSArray *fetchedObjects = [theContext executeFetchRequest:fetchRequest error:&error];
for (NSManagedObject *object in fetchedObjects)
{
[theContext deleteObject:object];
}
error = nil;
if(![theContext save:&error]){
NSLog(#"Couldn't save: %#", error);
}
What I don't understand is that for instance I download data and store it, and in the Settings I can see that my app uses 5MB of disk space.
Once I delete the data using this method, it says my app uses 6.3MB of data.
That makes absolutely no sense at all. What am I doing wrong? Why isn't the data being deleted correctly?
You can open the core data store with the SQLite vacuum option set to reclaim disk space. E.g.
NSDictionary *storeOptionsDict=#{NSSQLiteManualVacuumOption : #YES};
[persistentStoreCoordinator addPersistentStoreWithType: NSSQLiteStoreType configuration: nil URL: sourceStoreURL options: storeOptionsDict error: &error];
See this older question: How to VACUUM a Core Data SQLite db?
When using the sqlite store, the storage is managed by sqlite, not the core data framework itself. When you delete a managed object, the row is deleted from the appropriate table. In a typical database, this does not result in freeing any disk space. As for why the space used might actually increase, this speculation on my part, but I think it's because of the transaction logging. Even though the transaction is complete, the log is not immediately purged.
I would recommend that you not worry about this. The sqlite library will have internal management to compact tables (reclaim space from deleted rows) and purge old transaction records.

'NSObjectInaccessibleException' - CoreData sharing with TodayViewExtension

I've kinda implemented a today view extension with CoreData sharing in my app, I have multiple problems (like only one object showing when I have three) and a big one, "Terminating app due to uncaught exception 'NSObjectInaccessibleException', reason: 'CoreData could not fulfill a fault for '0xd0000000001c0004". Now, this only happens when I have the application open in the background, which leads me to believe that my app is leaving the store in a bad state, but that really shouldn't be happening. I'm using Persistent Stack, external 'library' to manage all of the CoreData (instead of AppDelegate) which is readable on https://gist.github.com/mluisbrown/7015953
CoreData Fetching from TodayView Extension:
-(void)viewDidLoad{
self.persistentStack = [[PersistentStack alloc] initWithStoreURL:self.storeURL modelURL:self.modelURL];
self.managedObjectContext = self.persistentStack.managedObjectContext;
}
- (NSURL*)storeURL
{
//NSURL* documentsDirectory = [[NSFileManager defaultManager] URLForDirectory:NSDocumentDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:NULL];
// return [documentsDirectory URLByAppendingPathComponent:#"WhatIOwe.sqlite"];
NSURL *directory = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:#"group.com.bittank.io"];
NSURL *storeURL = [directory URLByAppendingPathComponent:#"WhatIOwe.sqlite"];
return storeURL;
}
- (NSURL*)modelURL
{
return [[NSBundle mainBundle] URLForResource:#"WhatIOwe" withExtension:#"momd"];
}
- (NSFetchedResultsController *)fetchedResultsController {
self.persistentStack = [[PersistentStack alloc] initWithStoreURL:self.storeURL modelURL:self.modelURL];
self.managedObjectContext = self.persistentStack.managedObjectContext;
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription
entityForName:#"OweInfo" inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];
NSSortDescriptor *sort = [[NSSortDescriptor alloc]
initWithKey:#"details.date" ascending:NO];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:sort]];
[fetchRequest setFetchBatchSize:20];
NSFetchedResultsController *theFetchedResultsController =
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:managedObjectContext sectionNameKeyPath:nil
cacheName:#"Root"];
self.fetchedResultsController = theFetchedResultsController;
_fetchedResultsController.delegate = self;
return _fetchedResultsController;
}
As suggested, I've tried a merge policy in my persistent stack:
[self.managedObjectContext.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:self.storeURL
options:#{ NSPersistentStoreUbiquitousContentNameKey : #"WhatIOwe",
NSMigratePersistentStoresAutomaticallyOption : #YES,
NSInferMappingModelAutomaticallyOption : #YES,
NSMergeByPropertyObjectTrumpMergePolicy : #YES}
error:&error];
Another observation, on configuring my NSManagedObjectContext, passing:
[psc addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:self.storeURL options:nil error:&error]; allows the extension to read the store (but still throw the error I'm having), but passing
[psc addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:self.storeURL options:#{ NSPersistentStoreUbiquitousContentNameKey : #"iCloudStore",
NSMigratePersistentStoresAutomaticallyOption : #YES,
NSInferMappingModelAutomaticallyOption : #YES,
} error:&error]; will result in the extension not reading any data whatsoever.
Side-note: psc is passed as
__weak NSPersistentStoreCoordinator *psc = self.managedObjectContext.persistentStoreCoordinator;
self.persistentStoreCoordinator = self.managedObjectContext.persistentStoreCoordinator;
What is happening here is that you have two different processes accessing the same Core Data store, each with it's own NSPersistentStoreCoordinator.
One process has modified the store - most likely a delete. The other process has no way of knowing that this delete occurred, and already had an object in memory that pointed to the now deleted data. When that process tries to access that object it must go to the store to get the data ("firing a fault"), which no longer exists.
Core Data is not designed for this kind of use, and the capabilities of extensions are very different than applications. If your extension is able to write to the same data that the application can you may reconsider your approach and make the extension only able to read the data, and never hold the data in memory for long. This will at least mitigate the most common ways to run into these problems.
Replying to above answer and the question:
"Core Data is not designed for this kind of use"
It totally is. The assessment is correct: Something has likely been deleted in the actual app, and the extension is not aware. Fortunately CoreData provides a way to deal with this case. Check out the stalenessInterval property of NSManagedObjectContext. It defines how long your in memory cache is good for. If you're having problems because your in memory cache goes out of date from disk store change because an external process is changing them, simply set the staleness interval to 0 in your extension, and that will tell CoreData to always fetch new values from the store and ignore the in memory cache.
If you're holding a reference to an object in memory, and that object is deleted in the store, you still may have issues, so always check to make sure the object you are accessing has not been deleted.
There are a few other options if you want to get more detail. You could send notifications from your main app to your extension when things get saved to provide a manual trigger for reloading your data. You could also send specific object ids across that have been deleted or modified and use the refreshObject... method to manually refresh. Or check out mergeChangesFromContextDidSaveNotification:. You might be able to manually serialize the dictionary that expects and then send it across.
You could also have the parent app handle all database accesses and send results back via notifications. This might be unnecessary.
All of this requires a bit of work, but you're going to run into that with any
database system where the database is being accessed across multiple processes, and there is caching.
There are multiple issues with your code which are likely leading to a confused Core Data state. I can't be certain that they're causing your problem, but at the moment things are messed up badly enough that trying to debug this specific error is getting ahead of things. These problems include:
Confusion about how you're sharing data between your app and the extension.
You can do this using iCloud, or you can do it by using an app group to share the persistent store directly. You appear to be attempting both, which is not just unnecessary but also likely to cause problems keeping the extension up to date.
If you use iCloud to share data, you do not need the app group, because both the app and the extension will get their data from iCloud. You don't share a local persistent store in this case, instead the data is transferred via iCloud.
If you use an app group to share the persistent store file, you have no need of iCloud. The app and the extension both access the same file, which is located in the group container. Each can both read and write it.
Conflicting iCloud setups. You're using different values for NSPersistentStoreUbiquitousContentNameKey in different places. Even assuming that iCloud is working properly, this is going to prevent you from sharing data. If the app and extension are going to share via iCloud, they need to access the same cloud store, but you seem to be directing them to use separate cloud data stores.
You're not actually using the merge policy you're aiming for. You're passing NSMergeByPropertyObjectTrumpMergePolicy as one of the options when adding the persistent store file, but this isn't actually a valid option there. I would have expected at least a console message about this, but if there isn't one then it means Core Data is just silently ignoring that key. You set a merge policy by setting the value of the mergePolicy attribute on your managed object context. With no merge policy, you're falling back on the default NSErrorMergePolicy.
Unusual, suspicious code when adding the persistent store. In most cases with Core Data you'd add the persistent store and then later on create one or more managed object contexts. You appear to be creating the context first and only later adding a persistent store. That's not necessarily wrong but it's very unusual. If I were working on this code, it'd be a red flag to look over the life cycle of the Core Data stack very carefully. Especially if, as your code comments indicate, you're not getting any data at all in some cases.
Again I'm not sure if the above is directly causing this specific error, but they're going to be causing problems of various types and it wouldn't be surprising if the resulting confused state leads to this error.

CloudKit subscription with "CONTAINS" predicate

I am trying to setup a CloudKit subscription based on testing membership in an array.
The code I'm using to create the subscription is as follows:
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"users CONTAINS %#", userID];
CKSubscription *itemSubscription = [[CKSubscription alloc] initWithRecordType:#"foo"
predicate:predicate
options:CKSubscriptionOptionsFiresOnRecordCreation|CKSubscriptionOptionsFiresOnRecordUpdate];
CKNotificationInfo *notificationInfo = [[CKNotificationInfo alloc] init];
[notificationInfo setAlertLocalizationKey:#"Record notification"];
[notificationInfo setShouldBadge:YES];
[itemSubscription setNotificationInfo:notificationInfo];
[database saveSubscription:itemSubscription completionHandler:^(CKSubscription *subscription, NSError *error) {
NSLog(#"Error: %#, Subscription: %#", error, subscription);
}];
The log shows that the subscription is created successfully, however when I test by adding or changing a record via the CloudKit admin console I never get a notification on device.
I am able to receive notifications for subscriptions with other kinds of predicates (I've tested with a simple true predicate, and one that tests equality against a string field), so I know I have the notification code setup correctly.
I've also verified that my predicate listed above works when used in a fetch records query, so I know the predicate is setup correctly for the record type I have in CloudKit.
Has anyone been able to get subscription notifications with a predicate that tests for membership in an array?
After playing around, I found that unless I set notificationInfo.alertbody the subscription never fired.
So try setting it to "Test" first, then after set it to "". I found it continued to work after I set it to a blank string (though I didn't try setting it blank to start with), and as a blank string it doesn't show a notification, but my handler still gets called, which was what I was after.
With iOS 8.1.1 / Xcode 6.1.1

Core data query to many only shows 1 record

I have created a test project, to learn to work with Core Data. What I have done is, create two Entities:
Client
name
relationship --> projects (To Many relationship)
Project
project
relationship --> clients
I have created 2 UITableViewControllers one that displays all clients and one that will show all projects related to that client.
To show all related projects I have created this method:
-(NSArray *)relatedProjects:(Client *)client;
{
NSFetchRequest *request = [[NSFetchRequest alloc]init];
NSEntityDescription *e = [[model entitiesByName] objectForKey:#"Project"];
[request setEntity:e];
[request setPredicate:[NSPredicate predicateWithFormat:#"SELF IN %#", client.projects]];
NSSortDescriptor *sd = [NSSortDescriptor sortDescriptorWithKey:#"project"
ascending:YES];
[request setSortDescriptors:[NSArray arrayWithObject:sd]];
NSError *error;
NSArray *result = [context executeFetchRequest:request error:&error];
if (!result) {
[NSException raise:#"Fetch failed"
format:#"Reason: %#", [error localizedDescription]];
}
relatedProjects = [[NSMutableArray alloc] initWithArray:result];
//return result;
return relatedProjects;
}
Only problem is that is will only show 1 project instead of all related projects.
If I leave out this line, then all projects are shown, but then there is no relationship between the projects and client.
[request setPredicate:[NSPredicate predicateWithFormat:#"SELF IN %#", client.projects]];
(for my testcase I have created 1 client with several projects, so that I know it should show more then 1 project)
So I am stuck how to solve this issue, as there are several possibilities why this isn't working:
Predicate is not ok
Relationships between client and projects are not ok
etc
So if there is anyone who can give me some pointers, that would be great.
Under the assumption that:
you have many-to-many relationship between Client<<-->>Project (you should mark the relationship of both projects and clients as to-many in the editor).
You have set the inverse relationship correctly (in the model editor).
You are adding the the objects to the relationship correctly.
try changing the predicate to:
[NSPredicate predicateWithFormat:#"%# IN clients",client.objectID]
I prefer to create Clint model like
Client -
# clientId
# clientName
# createProjects //relation to project
Project
# projectId
# project
# createdBy // relation inverse to createProjects
When you are fetching the data first fetch client data and load tableView with the data. Once particular client selected fetch the projectDetails (using relation) and load tableView.
client.createdProjects will give you array of Project objects.
You're doing way to much work. If you've modelled the relationship in the model, Client already has a list of related projects. It will be named the same as the relationship.
When you fetch the Client object, the list of projects will be a 'fault', but as soon as you access it
NSArray* allProjects = client.projects;
Core Data will realise that it isn't populated yet and go and fetch the list for you.
Quick Summary:
-(NSArray *)relatedProjects:(Client *)client;
{
return client.projects;
}
Done.
This is in fact why you're using core data and not using sql directly ( among many other cool reasons).

Resources