For an app that fetches web from a web service, I have included a plist to be parsed into CoreData if its the first run because the data is not readily available in the Docs directory or may take long to fetch from the web. I do have NSNotifications signaling when a web fetch/synchronization has succeeded though.
At present in AppDelegate applicationDidFinishLaunchingWithOptions I call:
[self checkIfFirstRun];
which is this:
-(void)checkIfFirstRun{
NSString *bundleVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:(NSString *)kCFBundleVersionKey];
NSString *appFirstStartOfVersionKey = [NSString stringWithFormat:#"first_start_%#", bundleVersion];
NSNumber *alreadyStartedOnVersion = [[NSUserDefaults standardUserDefaults] objectForKey:appFirstStartOfVersionKey];
if(!alreadyStartedOnVersion || [alreadyStartedOnVersion boolValue] == NO) {
// IF FIRST TIME -> Preload plist data
UIAlertView *firstRun = [[UIAlertView alloc] initWithTitle:#"1st RUN USE LOCAL DB"
message:#"FIRST"
delegate:self
cancelButtonTitle:#"Cancel"
otherButtonTitles:#"Ok", nil];
[firstRun show];
NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
[prefs setObject:[NSNumber numberWithBool:YES] forKey:appFirstStartOfVersionKey];
[prefs synchronize];
//Use plist
[self parsePlistIntoCD];
} else {
UIAlertView *secondRun = [[UIAlertView alloc] initWithTitle:#"nTH RUN WEB FETCH"
message:#"nTH"
delegate:self
cancelButtonTitle:#"Cancel"
otherButtonTitles:#"Ok", nil];
[secondRun show];
}
}
So ok, i get my plist parsed perfectly into my CoreData db.
Here is the parsePlistIntoCD:
-(void)parsePlistIntoCD{
self.managedObjectContext = [[SDCoreDataController sharedInstance] backgroundManagedObjectContext];
// 3: Now put the plistDictionary into CD...create get ManagedObjectContext
NSManagedObjectContext *context = self.managedObjectContext;
NSError *error;
//Create Request & set Entity for request
NSFetchRequest *holidayRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *topicEntityDescription = [NSEntityDescription entityForName:#"Holiday" inManagedObjectContext:context];
[holidayRequest setEntity:topicEntityDescription];
//Create new NSManagedObject
//Holiday *holidayObjectToSeed = nil;
Holiday *newHoliday = nil;
//Execute fetch just to make sure?
NSArray *holidayFetchedArray = [context executeFetchRequest:holidayRequest error:&error];
if (error) NSLog(#"Error encountered in executing topic fetch request: %#", error);
// No holidays in database so we proceed to populate the database
if ([holidayFetchedArray count] == 0) {
//Get path to plist file
NSString *holidaysPath = [[NSBundle mainBundle] pathForResource:#"PreloadedFarsiman" ofType:#"plist"];
//Put data into an array (with dictionaries in it)
NSArray *holidayDataArray = [[NSArray alloc] initWithContentsOfFile:holidaysPath];
NSLog(#"holidayDataArray is %#", holidayDataArray);
//Get number of items in that array
int numberOfTopics = [holidayDataArray count];
//Loop thru array items...
for (int i = 0; i<numberOfTopics; i++) {
//get each dict at each node
NSDictionary *holidayDataDictionary = [holidayDataArray objectAtIndex:i];
//Insert new object
newHoliday = [NSEntityDescription insertNewObjectForEntityForName:#"Holiday" inManagedObjectContext:context];
//Parse all keys in each dict object
[newHoliday setValuesForKeysWithDictionary:holidayDataDictionary];
//Save and or log error
[context save:&error];
if (error) NSLog(#"Error encountered in saving topic entity, %d, %#, Hint: check that the structure of the pList matches Core Data: %#",i, newHoliday, error);
};
}
[[SDSyncEngine sharedEngine] startSync];
}
The thing is, I need to also make sure that if there is internet available, that my CoreData db get repopulated with the fetched web data.
But If I leave the call to [self parsePlistIntoCD]; only the plist data is present in the CoreData. First or nth run, I only get the plist data. If I comment that line out, I get my web fetched data.
Why doesnt the web fetched data replace the plist parsed data?
So the logic of parsePlistIntoCD is essentially
if no objects in store, load them from plist
always invoke startSync on [SDSyncEngine sharedEngine], which handles the web download and sync.
It looks to me like your startSync will in fact be invoked. So I would look there for the bug. You could add a log statement, or set breakpoints, to verify that that code path is actually being followed.
Both the plist parse and the web data fetch might take some time. That's a sign that you should be doing these operations in the background, perhaps with a GCD queue. You don't know in advance whether either of them will succeed. So don't set the preferences until they finish.
Side note: you can query the preferences database for BOOLs, making your code shorter, and therefore easier to read.
BOOL alreadyStartedOnVersion = [[NSUserDefaults standardUserDefaults] boolForKey:appFirstStartOfVersionKey];
and
[prefs setBool:YES forKey:appFirstStartOfVersionKey];
You can also replace numberWithBool: with simply #(YES) and #(NO).
For your program logic, I suggest something like this:
In -applicationDidFinishLaunchingWithOptions:, check to see if the starting plist data has been loaded. Forget about whether it's the first run. Just see whether the plist data needs to be loaded. Maybe call that shouldLoadPlistData. Or maybe you need to tie that to the version you're running, in which case you'd store a string latestPlistVersionLoaded.
If you haven't loaded it yet, enqueue a block to perform the plist load. At the conclusion of the plist load, set shouldLoadPlistData to NO, to note that plist data no longer needs to be loaded. If, for some reason, the plist load fails (maybe the phone runs out of battery or your app is killed by user or system), then on the next launch you're back where you started.
also check to see whether you have net access. If you do, enqueue a block to retrieve the web-based data, parse the data, and then, upon conclusion, update the preferences.
If the data is large, you might want to checkpoint this work:
Do I have the full web update? Then I'm done. Otherwise...
Has the download finished? Yay, I have the data, let's load it.
If not, have I started the download?
This staged checkpointing will also allow you to ask the system for extra time, if your app exits in the middle of the download.
parseListIntoCD feels a bit bloated to me. It does more than its name implies. Perhaps you could refactor it into a check (shouldLoadPlist), a method that does the import (importPlist:intoContext:), and a method that fires off the sync.
I strongly suggest that you pass the working NSManagedObjectContext in as a parameter, rather than having some global object that dispenses MOCs (as [SDCoreDataController sharedInstance] appears to do. It gives you much more control, and allows you to write unit tests much more easily. If you also pass in the path to the plist, you now have clean code that should behave the same way every time you call it.
Your use of the NSError ** parameter is consistently incorrect. The value of NSError is undefined upon success. You must test the result of the operation, not the value of the error, to determine whether you succeeded. The idiom is always
if (![someObject doTaskWithObject:foo error:&error]) {
// handle the error
}
Take a look also at countForFetchRequest:error. It would give you the same info that you're currently extracting by performing a fetch and counting results, but without having to instantiate the NSManagedObjects.
Related
I have one question near the end.
I am working from the belief/experience that seeding iCloud more than once is a bad idea and that if a user can do the wrong thing, he probably will sooner or later.
What I want to do:
A. When the user changes the app preference "Enable iCloud" from NO to YES, display AlertView asking (Yes or No) if the user wishes to seed the cloud with existing non-iCloud Data.
B. Ensure that the app seeds iCloud only once on an iCloud account, refraining to put up the AlertView once seeding is completed the first time.
My Method:
Following Apple's Docs concerning the proper use of NSUbiquitousKeyValueStore, I am using the following method in, - (void)application: dFLWOptions:
- (void)updateKVStoreItems:(NSNotification*)notification {
// Get the list of keys that changed.
NSDictionary* userInfo = [notification userInfo];
NSNumber* reasonForChange = [userInfo objectForKey:NSUbiquitousKeyValueStoreChangeReasonKey];
NSInteger reason = -1;
// If a reason could not be determined, do not update anything.
if (!reasonForChange)
return;
// Update only for changes from the server.
reason = [reasonForChange integerValue];
if ((reason == NSUbiquitousKeyValueStoreServerChange) ||
(reason == NSUbiquitousKeyValueStoreInitialSyncChange)) { // 0 || 1
// If something is changing externally, get the changes
// and update the corresponding keys locally.
NSArray* changedKeys = [userInfo objectForKey:NSUbiquitousKeyValueStoreChangedKeysKey];
NSUbiquitousKeyValueStore* store = [NSUbiquitousKeyValueStore defaultStore];
NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
// This loop assumes you are using the same key names in both
// the user defaults database and the iCloud key-value store
for (NSString* key in changedKeys) {//Only one key: #"iCloudSeeded" a BOOL
BOOL bValue = [store boolForKey:key];
id value = [store objectForKey:#"iCloudSeeded"];
[userDefaults setObject:value forKey:key];
}
}
}
Include the following code near the top of application: dFLWO:
NSUbiquitousKeyValueStore* store = [NSUbiquitousKeyValueStore defaultStore];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(updateKVStoreItems:)
name:NSUbiquitousKeyValueStoreDidChangeExternallyNotification
object:store]; // add appDelegate as observer
After loading iCloud Store, then seed it with non-iCloud data ONLY if seeding has never been done
- (BOOL)loadiCloudStore {
if (_iCloudStore) {return YES;} // Don’t load iCloud store if it’s already loaded
NSDictionary *options =
#{
NSMigratePersistentStoresAutomaticallyOption:#YES
,NSInferMappingModelAutomaticallyOption:#YES
,NSPersistentStoreUbiquitousContentNameKey:#"MainStore"
};
NSError *error=nil;
_iCloudStore = [_coordinator addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil URL:[self iCloudStoreURL] options:options error:&error];
if (_iCloudStore) {
NSUbiquitousKeyValueStore* store = [NSUbiquitousKeyValueStore defaultStore];
BOOL iCloudSeeded =
[store boolForKey:#"iCloudSeeded"];//If the key was not found, this method returns NO.
if(!iCloudSeeded) // CONTROL IS HERE
[self confirmMergeWithiCloud]; // Accept one USER confirmation for seeding in AlertView ONCE world wide
return YES; // iCloud store loaded.
}
NSLog(#"** FAILED to configure the iCloud Store : %# **", error);
return NO;
}
Once the seeding is completed do the following to prevent any repeat seeding:
if (alertView == self.seedAlertView) {
if (buttonIndex == alertView.firstOtherButtonIndex) {
[self seediCloud];
NSUbiquitousKeyValueStore* store = [NSUbiquitousKeyValueStore defaultStore];
[store setBool:YES forKey:#"iCloudSeeded"]; // NEVER AGAIN
//[store synchronize];
}
}
}
Be sure to get a total iCloud reset before the above process using:
[NSPersistentStoreCoordinator
removeUbiquitousContentAndPersistentStoreAtURL:[_iCloudStore URL]
options:options
error:&error])
This is a very tidy solution to my problem, IMHO, but I can not quite get it done.
MY QUESTION:
How do I respond to the first notification to updateKVStoreItems: above? It is a notification with bad info. I says the value is TRUE, but I have never set it to TRUE. How do I set default values for a key in NSUbiquitousKeyValueStore?
I find that the first notification is of reason : NSUbiquitousKeyValueStoreInitialSyncChange
When that note comes in, bValue is YES. THIS IS MY PROBLEM. It is as if, iCloud/iOS assumes any new BOOL to be TRUE.
I need this value to be NO initially so that I can go ahead and follow the Apple Docs and set
the NSUserDefault to NO. And then Later when the seeding is done, to finally set the value: YES for the key:#"iCloudSeeded"
I find I can not penetrate the meaning of the following from Apple:
NSUbiquitousKeyValueStoreInitialSyncChange
Your attempt to write to key-value storage was discarded because an initial download from iCloud has not yet happened.
That is, before you can first write key-value data, the system must ensure that your app’s local, on-disk cache matches the truth in iCloud.
Initial downloads happen the first time a device is connected to an iCloud account, and when a user switches their primary iCloud account.
I don't quite understand the implications of number 2 below, which I found online:
NSUbiquitousKeyValueStoreInitialSyncChange – slightly more complicated, only happens under these circumstances:
1. You start the app and call synchronize
2. Before iOS has chance to pull down the latest values from iCloud you make some changes.
3. iOS gets the changes from iCloud.
If this problem was with NSUserDefaults and not NSUbiquitousKeyValueStore, I believe I would need to go to registerDefaults.
I am almost there,
How do I do this please!
Thanks for reading, Mark
The code was looking for both
A. NSUbiquitousKeyValueStoreInitialSyncChange and
B. NSUbiquitousKeyValueStoreServerChange
I was unable to figure out what to do with the notifications. I know see that I did not need to do anything with either. My app only needs to read and write, in order to solve the problem I laid out in my question header.
The app gets the current value with:
NSUbiquitousKeyValueStore* store = [NSUbiquitousKeyValueStore defaultStore];
BOOL iCloudSeeded = [store boolForKey:#"iCloudSeeded"];
The app sets the value in the NSUbiquitousKeyValueStore with:
NSUbiquitousKeyValueStore* store = [NSUbiquitousKeyValueStore defaultStore];
[store setBool:YES forKey:#"iCloudSeeded"];
I believe I am correct in saying the following: Writing is done into memory. Very soon thereafter the data is put by the system onto disk.
From there it is taken and put into iCloud and is made available to the other devices running the same app on the same iCloud account. In the application I have described, no observer needs to be added, and
nothing else needs to be done. This is maybe an "unusual" use of NSUbiquitousKeyValueStore.
If you came here looking for a an more "usual" use, say when a user type something into a textview and it later
appears on a view of other devices running the same app, check out a simple demo I came across at :
https://github.com/cgreening/CMGCloudSyncTest
The better functioning (monitoring only) notification handler follows:
- (void)updateKVStoreItems:(NSNotification*)notification {
NSNumber *reason = notification.userInfo[NSUbiquitousKeyValueStoreChangeReasonKey];
if(!reason) return;
// get the reason code
NSInteger reasonCode = [notification.userInfo[NSUbiquitousKeyValueStoreChangeReasonKey] intValue];
BOOL bValue;
NSUbiquitousKeyValueStore *store;
switch(reasonCode) {
case NSUbiquitousKeyValueStoreServerChange:{ // code 0, monitoring only
store = [NSUbiquitousKeyValueStore defaultStore];
bValue = [store boolForKey:#"iCloudSeeded"];
id value = [store objectForKey:#"iCloudSeeded"];
DLog(#"New value for iCloudSeeded=%d\nNo Action need be take.",bValue);
// For monitoring set in UserDefaults
[[NSUserDefaults standardUserDefaults] setObject:value forKey:#"iCloudSeeded"];
break;
}
case NSUbiquitousKeyValueStoreAccountChange: {// ignore, log
NSLog(#"NSUbiquitousKeyValueStoreAccountChange");
break;
}
case NSUbiquitousKeyValueStoreInitialSyncChange:{ // ignore, log
NSLog(#"NSUbiquitousKeyValueStoreInitialSyncChange");
break;
}
case NSUbiquitousKeyValueStoreQuotaViolationChange:{ // ignore, log
NSLog(#"Run out of space!");
break;
}
}
}
Adding 9/3/14
So sorry but I continued to have trouble using a BOOL, I switched to an NSString and now
all is well.
METHOD TO ENSURE THAT THE "MERGE" BUTTON FOR SEEDING ICOUD IS USED AT MOST ONCE DURING APP LIFETIME
Use NSString and not BOOL in KV_STORE. No need to add observer, except for learning
In Constants.h :
#define SEEDED_ICLOUD_MSG #"Have Seeded iCloud"
#define ICLOUD_SEEDED_KEY #"iCloudSeeded"
Before calling function to seed iCloud with non-iCloud data:
NSUbiquitousKeyValueStore* kvStore = [NSUbiquitousKeyValueStore defaultStore];
NSString* strMergeDataWithiCloudDone =
[kvStore stringForKey:ICLOUD_SEEDED_KEY];
NSComparisonResult *result = [strMergeDataWithiCloudDone compare:SEEDED_ICLOUD_MSG];
if(result != NSOrderedSame)
//put up UIAlert asking user if seeding is desired.
If user chooses YES : set Value for Key after the merge is done.
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
if (alertView == self.seedAlertView) {
if (buttonIndex == alertView.firstOtherButtonIndex) {
[self seediCloudwithNoniCloudData];
NSUbiquitousKeyValueStore* kvStoretore = [NSUbiquitousKeyValueStore defaultStore];
[store setObject:SEEDED_ICLOUD_MSG forKey:ICLOUD_SEEDED_KEY];
}
}
}
Thereafter on all devices, for all time, the code
NSUbiquitousKeyValueStore* kvStoretore = [NSUbiquitousKeyValueStore defaultStore];
NSString* msg =
[kvStore stringForKey:ICLOUD_SEEDED_KEY];
produces: msg == SEEDED_ICLOUD_MESSAGE
For the life of me I can't work this one out, but CoreData keeps throwing me an error.
Cannot delete object that was never inserted.
Here is the jist of my app cycle:
1/ Push ViewController.
2/ Get managed object context from app delegate.
FLAppDelegate *appDelegate = (FLAppDelegate *)[[UIApplication sharedApplication] delegate];
self.managedObjectContext = appDelegate.managedObjectContext;
3/ Check if Session exists.
4/ No Session exists, create a new one.
self.session = nil;
self.session = [NSEntityDescription insertNewObjectForEntityForName:#"Session" inManagedObjectContext:self.managedObjectContext];
//Set attributes etc...
//Keep a reference to this session for later
[[NSUserDefaults standardUserDefaults] setURL:self.session.objectID.URIRepresentation forKey:kKeyStoredSessionObjectIdUriRep];
[[NSUserDefaults standardUserDefaults] synchronize];
NSError *error = nil;
if (![self.managedObjectContext save:&error])
{
//Handle error if save fails
}
5/ Pop ViewController.
6/ Return to ViewController.
7/ Again, check if Session exists.
8/ A Session is found! (By looking at NSUserDefaults for the one we stored to later reference). So I get Session I created earlier then give the user a choice to delete that one and start fresh or continue with that one.
NSURL *url = [[NSUserDefaults standardUserDefaults] URLForKey:kKeyStoredSessionObjectIdUriRep];
if (url) //Found existing flight session
{
NSManagedObjectID *objId = [self.managedObjectContext.persistentStoreCoordinator managedObjectIDForURIRepresentation:url];
NSManagedObject *obj = [self.managedObjectContext objectWithID:objId];
self.session = (Session *)obj;
//Ask the user if they want to continue with this session or discard and start a new one
}
9/ Choose to delete this Session and start a new one.
10/ Problem begins here! I delete the reference I am keeping to this Session as it is no longer relevant and try to delete the object then save those changes.
[[NSUserDefaults standardUserDefaults] removeObjectForKey:kKeyStoredSessionObjectIdUriRep];
[[NSUserDefaults standardUserDefaults] synchronize];
[self.managedObjectContext deleteObject:self.session];
NSError *error = nil;
if (![self.managedObjectContext save:&error])
{
//Error!
}
11/ There we have it, when I try to run the save method, it crashes and throws an error: NSUnderlyingException = "Cannot delete object that was never inserted.";
I have no clue why it says so, as I appear to save the object whenever I create one and create the reference, retrieve the object from that reference but then deleting just breaks everything.
The problem is in your step 4. In this step you
Create the Session
Save its managed object ID to user defaults
Save changes to your managed object context.
The reason this fails is that when you create a new managed object, it has a temporary object ID that is only valid until you save changes. As soon as you save changes, it gets a new, permanent object ID. You're saving the temporary object ID, but when you look it up later it's not valid any more.
You can fix this just by changing the order of operations in step 4 so that you:
Create the Session
Save changes to your managed object context
Save the Session's object ID to user defaults
This way you'll have a permanent ID when you save the value to user defaults.
I am trying my hand at some very basic implementation of MagicalRecord to get the hang of it and run into the following.
When I save an entry and then fetch entries of that type it will come up with the entry I just saved. However, when I save the entry, close the app, start it again, and then fetch, it comes up empty.
Code for saving:
- (void)createTestTask{
NSManagedObjectContext *localContext = [NSManagedObjectContext contextForCurrentThread];
Task *task = [Task createInContext:localContext];
task.tName = #"First Task";
task.tDescription = #"First Task created with MagicalRecord. Huzzah!";
NSError *error;
[localContext save:&error];
if (error != Nil) {
NSLog(#"%#", error.description);
}
}
Code for fetching: (all I want to know here if anything is actually saved)
- (void) fetchTasks{
NSArray *tasks = [Task findAll];
NSLog(#"Found %d tasks", [tasks count]);
}
I am sure I am missing something here, but not anything I can seem to find on stackoverflow or in the Tutorials I looked at.
Any help is welcome.
I have to ask the obvious "Is it plugged in" question: Did you initialize the Core Data Stack with one of the +[MagicalRecord setupCoreDataStack] methods?
Did your stack initialize properly? That is, is your store and model compatible? When they aren't, MagicalRecord (more appropriately, Core Data) will set up the whole stack without the Persistent Store. This is annoying because it looks like everything is fine until it cannot save to the store...because there is no store. MagicalRecord has a +[MagicalRecord currentStack] method that you can use to examine the current state of the stack. Try that in the debugger after you've set up your stack.
Assuming you did that, the other thing to check is the error log. If you use
[localContext MR_saveToPersistentStoreAndWait];
Any errors should be logged to the console. Generally when you don't see data on a subsequent run of your app, it's because data was not saved when you thought you called save. And the save, in turn, does not happen because your data did not validate correctly. A common example is if you have a required property, and it's still nil at the time you call save. "Normal" core data does not log these problems at all, so you might think it worked, when, in fact, the save operation failed. MagicalRecord, on the other hand, will capture all those errors and log them to the console at least telling you what's going on with your data.
When i have started with magical record I was also facing this problem, problem is context which you are using to save data. here is my code which might help you
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) {
NSArray *userInfoArray = [UserBasicInfo findByAttribute:#"userId" withValue:[NSNumber numberWithInt:loggedInUserId] inContext:localContext];
UserBasicInfo* userInfo;
if ([userInfoArray count]) {
userInfo = [userInfoArray objectAtIndex:0];
} else {
userInfo = [UserBasicInfo createInContext:localContext];
}
userInfo.activeUser = [NSNumber numberWithBool:YES];
userInfo.firstName = self.graphUser[#"first_name"];
userInfo.lastName = self.graphUser[#"last_name"];
userInfo.userId = #([jsonObject[#"UserId"] intValue]);
userInfo.networkUserId = #([jsonObject[#"NetworkUserId"] longLongValue]);
userInfo.userPoint = #([jsonObject[#"PointsEarned"] floatValue]);
userInfo.imageUrl = jsonObject[#"Picturelist"][0][#"PictureUrL"];
userInfo.imageUrlArray = [NSKeyedArchiver archivedDataWithRootObject:jsonObject[#"Picturelist"]];
} completion:^(BOOL success, NSError *error) {
}];
Use this when your done
[[NSManagedObjectContext MR_defaultContext]saveToPersistentStoreAndWait];
I have an app that works fine on the simulator 6.1, works fine on the iPhone5 and iPad3 on iOS6.1 but when run on iPhone4S it crashes in this method with Exc Bad Access:
-(void)parsePlistIntoCD{
self.managedObjectContext = [[SDCoreDataController sharedInstance] backgroundManagedObjectContext];
// 3: Now put the plistDictionary into CD...create get ManagedObjectContext
NSManagedObjectContext *context = self.managedObjectContext;
NSError *error;
//Create Request & set Entity for request
NSFetchRequest *holidayRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *topicEntityDescription = [NSEntityDescription entityForName:#"Holiday" inManagedObjectContext:context];
[holidayRequest setEntity:topicEntityDescription];
//Create new NSManagedObject
//Holiday *holidayObjectToSeed = nil;
Holiday *newHoliday = nil;
//Execute fetch just to make sure?
NSArray *holidayFetchedArray = [context executeFetchRequest:holidayRequest error:&error];
**if (error) NSLog(#"Error encountered in executing topic fetch request: %#", error); // if I comment this line out it reaches as far as the next bold line**
// No holidays in database so we proceed to populate the database
if ([holidayFetchedArray count] == 0) {
//Get path to plist file
NSString *holidaysPath = [[NSBundle mainBundle] pathForResource:#"PreloadedFarsiman" ofType:#"plist"];
//Put data into an array (with dictionaries in it)
NSArray *holidayDataArray = [[NSArray alloc] initWithContentsOfFile:holidaysPath];
**NSLog(#"holidayDataArray is %#", holidayDataArray);**
//Get number of items in that array
int numberOfTopics = [holidayDataArray count];
//Loop thru array items...
for (int i = 0; i<numberOfTopics; i++) {
//get each dict at each node
NSDictionary *holidayDataDictionary = [holidayDataArray objectAtIndex:i];
//Insert new object
newHoliday = [NSEntityDescription insertNewObjectForEntityForName:#"Holiday" inManagedObjectContext:context];
//Parse all keys in each dict object
[newHoliday setValuesForKeysWithDictionary:holidayDataDictionary];
//Save and or log error
[context save:&error];
if (error) NSLog(#"Error encountered in saving topic entity, %d, %#, Hint: check that the structure of the pList matches Core Data: %#",i, newHoliday, error);
};
}
//set bool that specifies the coredata has been populated from plist already
NSString *bundleVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:(NSString *)kCFBundleVersionKey];
NSString *appFirstStartOfVersionKey = [NSString stringWithFormat:#"first_start_%#", bundleVersion];
NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
[prefs setObject:#(YES) forKey:appFirstStartOfVersionKey];
[prefs synchronize];
}
Why only on 4S? It gives no console log and the last know method traversed in this one above. Here is a pic:
And as mentioned above, if I uncomment that NSLog line it reaches as far as logging the array, as shown in the pic. If I leave it in it stops at that line.
NSArray *holidayFetchedArray = [context executeFetchRequest:holidayRequest error:&error];
if (error)
NSLog(#"Error encountered in executing topic fetch request: %#", error);
No no no no no. I know this is a difficult pattern, but please let's try to do it right. Not if (error). error could be anything (esp. under non-ARC). The test is if (!holidayFetchedArray).
For all of these methods that return a value and also take an NSError** by indirection, you test the result to see if it is nil. If it is, then there was an error because returning nil is the sign that there was an error. Then and only then you may touch the error meaningfully.
The docs are always quite clear about this, though it is true that a fog can come over one's eyes at the critical instance, so I've added some comments in italic brackets to call out the key points:
request
A fetch request that specifies the search criteria for the
fetch.
error
If there is a problem executing the fetch, upon return
contains an instance of NSError that describes the problem.
[And if there is no problem executing the fetch, contains garbage so don't touch it!]
Return Value
An array of objects that meet the criteria specified by request
fetched from the receiver and from the persistent stores associated
with the receiver’s persistent store coordinator. If an error occurs,
returns nil. [And that, forsooth, is the sign that an error did occur.] If no objects match the criteria specified by request,
returns an empty array.
This might be the cause of your trouble or it might not, but you must fix it now. I want you to go through all your code looking for NSError* variables declared for use in this pattern and fix all of them! Thank you. Here endeth the lesson.
This is a stab in the dark b/c you haven't identified the line of code where the crash occurs. I recommended you set a breakpoint and then step through until you hit the crash. If what I say below doesn't resolve the issue then edit your post and add more info about where the crash occurs and we'll go from there.
Here's something to check. The line:
[newHoliday setValuesForKeysWithDictionary:holidayDataDictionary]
may be crashing if any of the keys in holidayDataDictionary aren't valid for an instance of the Holiday class.
With regard to your question about why it only crashes on your iPhone 4S there's not enough data to explain that yet. Depending on how you've been doing testing and core data migration (if at all) you may have a model inconsistency on that device, though I don't see anything in the screen shot you posted pointing me in that direction. I would try uninstalling/reinstalling your app and see if the crash is still iPhone 4S only.
I'm currently stuck with a weird problem with core data. The app I'm writing downloads a bunch of data from the server, which is translated into core data objects and stored. The device can also create new objects and upload them to the server. One of these objects is a Document which is essentially a representation of a file.
The model for this is MPDocument. A document can also be linked to an MPPlace model, and an MPUser model (users create documents, and documents belong to places).
I'm having no problem downloading the objects from the server, and all relationships are being created and assigned correctly. The problem lies when I try to create a new document on the device itself. The document gets created, and I set all of the relationships, the document gets uploaded and everything seems fine. But when I check the database through a core-data viewer tool, all of the document objects have no value for the place relationship. This happens to all the existing documents, not just the new one. I really can't figure out what's going on!
I'm creating the document like so :
MPUser *current = [MPUser currentUser];
MPDocument *doc = [[MPDocument alloc] init];
doc.name = #"App Upload";
doc.local_url = [NSString stringWithFormat:#"%#", [info valueForKey:UIImagePickerControllerReferenceURL]];
doc.local_url_type = #(MPDocumentUrlTypeAsset);
doc.user = current;
[current addCreatedDocumentsObject:doc];
[doc setValue:self.place forKey:#"place"];
[self.place addDocumentsObject:doc];
I then have a document uploader which handles all uploading :
MPDocumentUploader *uploader = [[MPDocumentUploader alloc] initWithDocument:doc];
uploader.requestDelegate = self;
uploader.successBlock = ^(MPDocumentUploader *uploader, MPDocument *doc) {
NSLog(#"Got doc = %#", doc);
};
[uploader upload];
When the success block is called, the document object DOES have the place relationship set. So even once the upload has finished, the place is set, so I'm really confused now as to where the relationships are being completely cleared.
The document uploader looks something like this :
- (void) upload
{
.... retrieve the local file and turn into NSData. This is fine
MPRequest *request = [MPRequest requestWithURL:_url];
[MPUser signRequest:request];
[request setDelegate:_requestDelegate];
[request setRequestMethod:#"POST"];
[request mountDocumentUploader:self];
[request submit:^(MPResponse *resp, NSError *error) {
if (!error) {
NSDictionary *data = (NSDictionary *)[resp paramForKey:#"data"];
if (data) {
NSLog(#"Document = %#", _document);
_document.url = [data objectForKey:#"url"];
_document.objID = [data objectForKey:#"id"];
[_document saveLocally];
}
if (_successBlock) {
_successBlock(self, _document);
}
} else {
if (_failBlock) {
_failBlock(self, error);
}
}
}];
}
The MPRequest class handles all the actual uploading and server requests, but doesn't actually touch the MPDocument object.
I can't figure out what's going on or why it's clearing out the relationships. Please can someone help!?
Update
I've played around, and found that the error occurs when the submit block is called. Commenting out
_document.url = [data objectForKey:#"url"];
_document.objID = [data objectForKey:#"id"];
[_document saveLocally];
works as it is meant to, but now those values obviously aren't set. Adding either of those lines back in 1 at a time in isolation still causes the problem, so it seems that simply editing it at all is breaking it. Still no clue why though :(
ok well I'm not entirely sure what happened or why, but between fiddling with the code, cleaning the project, and restarting my laptop, it seems to have fixed itself so not a clue what the issue was but it currently seems to be ok. Very confused :S