Using iCloud for app database backup, but not syncing - ios

I'm seeking advice on using iCloud in a limited way. The docs are focused on syncing between multiple devices, which I do not need. When I say iCloud, I'm also including iCloud Drive, if that matters.
My iPhone app stores its data and state in a single SQLite database file that I want to save to iCloud (or iCloud Drive) for the purpose of backup, not for syncing changes between multiple devices. At launch, SQLite opens the database file and uses it synchronously (I'm using FMDB). The database file should remain on the device so, if iCloud is down, SQLite won't know or care. iCloud would have a shadow copy.
The iCloud copy does not always need to be current -- I'm okay with programmatically initiating an update to iCloud, and the restore if necessary. If the local database file becomes corrupted or if the user deletes the app, I would restore the file from iCloud. The file would usually be smaller than 1 MB, but might be large (100 MB), but mostly invariant.
I'm not syncing because of the data integrity risks of sharing whole SQLite files, and I cannot convert to Core Data (it's a huge app using FMDB).
I know that the iOS does daily backups to iCloud, and that would be sufficient if I could restore just my app from iCloud (and not the entire device).
I need advice on using iCloud for a shadow backup and restore, but not syncing.
UPDATE: I have made progress on this issue; I can save and restore my real SQLite file to/from iCloud. I subclassed UIDocument with these two overrides:
#implementation MyDocument
// Override this method to return the document data to be saved.
- (id)contentsForType:(NSString *)typeName error:(NSError * _Nullable *)outError
{
NSString *databasePath = ... // path to my SQLite file in Documents;
NSData *dbData = [NSData dataWithContentsOfFile: databasePath];
return dbData; // the entire database file
}
// Override this method to load the document data into the app’s data model.
- (BOOL)loadFromContents:(id)contents ofType:(NSString *)typeName error:(NSError * _Nullable *)outError
{
NSString *databasePath = ... // path to my SQLite file in Documents;
NSData *dbData = contents;
BOOL ret = [dbData writeToFile: databasePath atomically:YES];
return YES;
}
#end
My SQLite database file is in Documents and it stays there, but I write the shadow copy to iCloud thusly:
- (void)sendToICloud
{
NSURL *ubiq = [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil];
NSString *iCloudFileName = #"..."; // a file name I choose
NSURL *ubiquitousPackage = [[ubiq URLByAppendingPathComponent:#"Documents"]
URLByAppendingPathComponent:iCloudFileName];
MyDocument *myDoc = [[MyDocument alloc] initWithFileURL:ubiquitousPackage];
[myDoc saveToURL:[myDoc fileURL] forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
if ( success ) {
[myDoc closeWithCompletionHandler:^(BOOL success) {
}];
} else {
NSLog(#"iCloud write Syncing FAILED with iCloud");
}
}];
}
Now my issues are:
I have created a custom container in Xcode that appears in my entitlements file and in the Dev portal for the App ID. I can save documents to iCloud from either of my two devices (iPhone and iPad). But, my two devices do not see each other's files. There are numerous old threads about this with random solutions, but nothing has solved the problem.
The iCloud documentation says files are updated incrementally, but I don't know if that applies in my case. If I use a new filename for MyDocument (my iCloudFileName, above), I realize that an entire new file will be sent to iCloud. But, if I reuse the previous filename and send an updated NSData each time (as I'm doing in loadFromContents:ofType:error:), will iOS only send the parts that have changed since the last time I saved the file?

Another subject line now would be: Using iCloud for file backup across multiple devices.
I've solved my primary issue of seeing iCloud data sent by multiple devices. According to WWDC 2015 "Building Document Based App," to see all files (even those from other devices), use NSMetadataQuery, not NSFileManager. Also, the video presentation includes a warning about using NSURL because documents might be moved (use Bookmarks), but that doesn't affect my use case.
I still don't know if sending partially-changed NSData will cause incremental updates.
My UIDocument overrides and my sendToICloud method remain the same. What's new is my iCloud search.
If you aren't writing to the default iCloud Documents directory, writing will fail if the directory does not exist.
It's easier to just create it every time, thusly:
- (void)writeToiCloud
{
// ...
#define UBIQUITY_ID #"iCloud.TEAMID.bundleID.appName"
NSFileManager *defaultManager = [NSFileManager defaultManager];
NSURL *ubiq = [defaultManager URLForUbiquityContainerIdentifier:UBIQUITY_ID];
NSURL *ubiquitousFolder = [ubiq URLByAppendingPathComponent:#"MyFolder"];
[defaultManager createDirectoryAtURL:ubiquitousFolder
withIntermediateDirectories:YES
attributes:nil error:nil];
// ...
// ... forSaveOperation:UIDocumentSaveForCreating ...
}
- (void)updateFileList
{
// Use an iVar so query stays in scope
// Instance variables maintain a strong reference to the objects by default
query = [[NSMetadataQuery alloc] init];
[query setSearchScopes:#[ // use both for testing, then only DocumentsScope
NSMetadataQueryUbiquitousDataScope, // not in Documents
NSMetadataQueryUbiquitousDocumentsScope, // in Documents
]
];
// add a predicate if desired to restrict the search.
// No predicate finds everything, but Apple says:
// a query can’t be started if ... no predicate has been specified.
NSPredicate *predicate = [NSPredicate predicateWithFormat: #"%K like '*'", NSMetadataItemFSNameKey];
// NSPredicate *predicate = [NSPredicate predicateWithFormat: #"%K CONTAINS %#", NSMetadataItemPathKey, #"/MyFolder/"];
// NSPredicate *predicate = [NSPredicate predicateWithFormat: #"%K CONTAINS %#", NSMetadataItemPathKey, #"/Documents/"];
[query setPredicate:predicate];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(queryDidFinish:)
name:NSMetadataQueryDidFinishGatheringNotification
object:query];
[query startQuery];
}
- (void)queryDidFinish:(NSNotification *)notification
{
// iVar: NSMetadataQuery *query
for ( NSMetadataItem *item in [query results] ) {
NSURL *itemURL = [item valueForAttribute:NSMetadataItemURLKey];
NSLog(#"fileName: %#",itemURL.lastPathComponent);
// can't use getResourceValue:forKey:error
// that applies to URLs that represent file system resources.
if ( [itemURL.absoluteString hasSuffix:#"/"] ) {
continue; // skip directories
}
// process the itemURL here ...
}
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
});
[[NSNotificationCenter defaultCenter] removeObserver:self
name:NSMetadataQueryDidFinishGatheringNotification
object:query];
}

Related

Download Multiple Images Sequentially using NSURLSession downloadTask in Objective C

My app offers the option to download 3430 high resolution images from our server, each image of size 50k - 600k bytes.
The original approach was to just download all of them - but we realized that gave a lot of NSURLErrorTimedOut errors and crashed our program. We then implemented it such that we download all of the images, but in batches of 100 images at a time. Someone on SO suggested we actually implement our download like this:
Create a list of all file URLs that need to be downloaded.
Write your code so that it downloads these URLs sequentially. I.e. do
not let it start downloading a file until the previous one has
finished (or failed and you decided to skip it for now).
Use NSURLSession's support for downloading an individual file to a
folder, don't use the code to get an NSData and save the file
yourself. That way, your application doesn't need to be running while
the download finishes.
Ensure that you can tell whether a file has already been downloaded or
not, in case your download gets interrupted, or the phone is restarted
in mid-download. You can e.g. do this by comparing their names (if
they are unique enough), or saving a note to a plist that lets you
match a downloaded file to the URL where it came from, or whatever
constitutes an identifying characteristic in your case.
At startup, check whether all files are there. If not, put the missing
ones in above download list and download them sequentially, as in #2.
Before you start downloading anything (and that includes downloading
the next file after the previous download has finished or failed), do
a reachability check using the Reachability API from Apple's
SystemConfiguration.framework. That will tell you whether the user has
a connection at all, and whether you're on WiFi or cellular (in
general, you do not want to download a large number of files via
cellular, most cellular connections are metered).
We create a list of all images to download here:
- (void)generateImageURLList:(BOOL)batchDownloadImagesFromServer
{
NSError* error;
NSFetchRequest* leafletURLRequest = [[[NSFetchRequest alloc] init] autorelease];
NSEntityDescription* leafletURLDescription = [NSEntityDescription entityForName:#"LeafletURL" inManagedObjectContext:managedObjectContext];
[leafletURLRequest setEntity:leafletURLDescription];
numberOfImages = [managedObjectContext countForFetchRequest:leafletURLRequest error:&error];
NSPredicate* thumbnailPredicate = [NSPredicate predicateWithFormat:#"thumbnailLocation like %#", kLocationServer];
[leafletURLRequest setPredicate:thumbnailPredicate];
self.uncachedThumbnailArray = [managedObjectContext executeFetchRequest:leafletURLRequest error:&error];
NSPredicate* hiResPredicate = [NSPredicate predicateWithFormat:#"hiResImageLocation != %#", kLocationCache];
[leafletURLRequest setPredicate:hiResPredicate];
self.uncachedHiResImageArray = [managedObjectContext executeFetchRequest:leafletURLRequest error:&error];
}
We use NSURLSession to download an individual image to a folder by calling hitServerForUrl and implementing didFinishDownloadingToURL:
- (void)hitServerForUrl:(NSURL*)requestUrl {
NSURLSessionConfiguration *defaultConfigurationObject = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *defaultSession = [NSURLSession sessionWithConfiguration:defaultConfigurationObject delegate:self delegateQueue: nil];
NSURLSessionDownloadTask *fileDownloadTask = [defaultSession downloadTaskWithURL:requestUrl];
[fileDownloadTask resume];
}
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
if (isThumbnail)
{
leafletURL.thumbnailLocation = kLocationCache;
}
else
{
leafletURL.hiResImageLocation = kLocationCache;
}
// Filename to write to
NSString* filePath = [leafletURL pathForImageAtLocation:kLocationCache isThumbnail:isThumbnail isRetina:NO];
// If it's a retina image, append the "#2x"
if (isRetina_) {
filePath = [filePath stringByReplacingOccurrencesOfString:#".jpg" withString:#"#2x.jpg"];
}
NSString* dir = [filePath stringByDeletingLastPathComponent];
[managedObjectContext save:nil];
NSError* error;
[[NSFileManager defaultManager] createDirectoryAtPath:dir withIntermediateDirectories:YES attributes:nil error:&error];
NSURL *documentURL = [NSURL fileURLWithPath:filePath];
NSLog(#"file path : %#", filePath);
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
//Remove the old file from directory
}
[[NSFileManager defaultManager] moveItemAtURL:location
toURL:documentURL
error:&error];
if (error){
//Handle error here
}
}
This code calls loadImage, which calls `hitServer:
-(void)downloadImagesFromServer{
[self generateImageURLList:NO];
[leafletImageLoaderQueue removeAllObjects];
numberOfHiResImageLeft = [uncachedHiResImageArray count];
for ( LeafletURL* aLeafletURL in uncachedHiResImageArray)
{
//// Do the same thing again, except set isThumb = NO. ////
LeafletImageLoader* hiResImageLoader = [[LeafletImageLoader alloc] initWithDelegate:self];
[leafletImageLoaderQueue addObject:hiResImageLoader]; // do this before making connection!! //
[hiResImageLoader loadImage:aLeafletURL isThumbnail:NO isBatchDownload:YES];
//// Adding object to array already retains it, so it's safe to release it here. ////
[hiResImageLoader release];
uncachedHiResIndex++;
NSLog(#"uncached hi res index: %ld, un cached hi res image array size: %lu", (long)uncachedHiResIndex, (unsigned long)[uncachedHiResImageArray count]);
}
}
- (void)loadImage:(LeafletURL*)leafletURLInput isThumbnail:(BOOL)isThumbnailInput isBatchDownload:(BOOL)isBatchDownload isRetina:(BOOL)isRetina
{
isRetina_ = isRetina;
if (mConnection)
{
[mConnection cancel];
[mConnection release];
mConnection = nil;
}
if (mImageData)
{
[mImageData release];
mImageData = nil;
}
self.leafletURL = leafletURLInput;
self.isThumbnail = isThumbnailInput;
NSString* location = (self.isThumbnail) ?leafletURL.thumbnailLocation :leafletURL.hiResImageLocation;
//// Check if the image needs to be downloaded from server. If it is a batch download, then override the local resources////
if ( ([location isEqualToString:kLocationServer] || (isBatchDownload && [location isEqualToString:kLocationResource])) && self.leafletURL.rawURL != nil )
{
//NSLog(#"final loadimage called server");
//// tell the delegate to get ride of the old image while waiting. ////
if([delegate respondsToSelector:#selector(leafletImageLoaderWillBeginLoadingImage:)])
{
[delegate leafletImageLoaderWillBeginLoadingImage:self];
}
mImageData = [[NSMutableData alloc] init];
NSURL* url = [NSURL URLWithString:[leafletURL pathForImageOnServerUsingThumbnail:self.isThumbnail isRetina:isRetina]];
[self hitServerForUrl:url];
}
//// if not, tell the delegate that the image is already cached. ////
else
{
if([delegate respondsToSelector:#selector(leafletImageLoaderDidFinishLoadingImage:)])
{
[delegate leafletImageLoaderDidFinishLoadingImage:self];
}
}
}
Currently, I'm trying to figure out how to download the images sequentially, such that we don't call hitServer until the last image is finished downloading. Do I need to be downloading in the background? Thank you for suggestions!
My app offers the option to download 3430 high resolution images from our server, each image of size 50k - 600k bytes.
This seems like a job for on-demand resources. Just turn these files into on-demand resources obtained from your own server, and let the system take care of downloading them in its own sweet time.
This sounds very much like an architectural issue. If you fire off downloads without limiting them of course you're going to start getting timeouts and other things. Think about other apps and what they do. Apps that give the user the ability to do multiple downloads often limit how may can occur at once. iTunes for example can queue up thousands of downloads, but only runs 3 at a time. Limiting to just one at a time will only slow things down for your users. You need a balance that consider your user's available bandwidth.
The other part of this is to again consider what your users want. Does every one of your uses want every single image? I don't know what you are offering them, but in most apps which access resources like images or music, it's up to the user what and when they download. Thus they only download what they are interested in. So I'd recommend only downloading what the users are viewing or have somehow requested they want to download.

Magical Record - duplicate records appearing even if DB file is deleted

In my ios app, I am using Magical Record and NSFetchedResultsController. I am trying to implement below functionality:
User navigates to settings screen
He selects - 'Delete Account'
All his data is deleted
He is navigated to re-registration screen
To delete all his data I wrote below code:
- (void)cleanAndResetupDB
{
[MagicalRecord cleanUp];
BOOL isSuccess = YES;
for (NSString *dbStore in [self dbBackups]) {
NSError *error = nil;
NSURL *storeURL = [NSPersistentStore MR_urlForStoreName:dbStore];
if(![[NSFileManager defaultManager] removeItemAtURL:storeURL error:&error]){
NSLog(#"An error has occurred while deleting %#", dbStore);
NSLog(#"Error description: %#", error.description);
isSuccess = NO;
}
}
if (isSuccess) {
[MagicalRecord setupCoreDataStackWithStoreNamed:CRP_DB];
}
}
- (NSArray *)dbBackups
{
NSString *shmFileName = [NSString stringWithFormat:#"%#-shm",CRP_DB];
NSString *walFileName = [NSString stringWithFormat:#"%#-wal",CRP_DB];
return #[CRP_DB,shmFileName,walFileName];
}
When registration is complete user is navigated to contacts screen, where we retrieve related contacts from server and store it in local DB. Since FRC is used to retrieve data from local DB and show it in table view, as soon as data is saved in db it automatically appears in table view.
Problem is-
If I quit the app after removing local db, on relaunch it shows proper records, but if I don't quit the app after removing local db, then it shows duplicate records.
Any clues?
If you are using Core Data and you want to remove your database, you have to actually remove your persistent store. Simply deleting the database files is not enough. Core Data caches objects in memory and if it doesn't know that they should be deleted, they could be re-committed to the database. In particular, you are missing the call to removePersistentStore:error:.
NSPersistentStoreCoordinator *storeCoordinator = ...; // you should already have a persistent store coordinator
NSURL *storeURL = [NSPersistentStore MR_urlForStoreName:dbStore];
[storeCoordinator removePersistentStore:store error:&error];
[[NSFileManager defaultManager] removeItemAtPath:storeURL.path error:&error];

How do you migrate an existing core data iOS 7 app's data into iCloud?

I have an iOS 7 app that already uses Core Data. I have used the new iOS 7 method of integrating iCloud into my app to sync items stored in core data by using the following code as an example:
https://github.com/mluisbrown/iCloudCoreDataStack/blob/master/README.md
This works great, except that all of the original data on the device doesn't show up in the iCloud store. I keep hearing that I need to migrate the data - but I can't find any examples on how to do this properly. Does anyone know how to do this?
I keep getting pointed to using migratePersistentStore:toURL:options:withType:error:, but I don't see how I can use this...
Here is a sample app with a iCloud control panel to move the store to or from iCloud. To move your existing store you need to open it with the existing options but make sure you use iOS7 options for the target store. Take a look at the sample apps code in OSCDStackManager and if you have specific questions then post them. http://ossh.com.au/design-and-technology/software-development/sample-library-style-ios-core-data-app-with-icloud-integration/
- (bool)moveStoreFileToICloud:(NSURL*)fileURL delete:(bool)shouldDelete backup:(bool)shouldBackup {
FLOG(#" called");
// Always make a backup of the local store before migrating to iCloud
if (shouldBackup)
[self backupLocalStore];
NSPersistentStoreCoordinator *migrationPSC = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.managedObjectModel];
// Open the existing local store using the original options
id sourceStore = [migrationPSC addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:fileURL options:[self localStoreOptions] error:nil];
if (!sourceStore) {
FLOG(#" failed to add old store");
return FALSE;
} else {
FLOG(#" Successfully added store to migrate");
bool moveSuccess = NO;
NSError *error;
FLOG(#" About to migrate the store...");
// Now migrate the store using the iCloud options
id migrationSuccess = [migrationPSC migratePersistentStore:sourceStore toURL:[self icloudStoreURL] options:[self icloudStoreOptions] withType:NSSQLiteStoreType error:&error];
if (migrationSuccess) {
moveSuccess = YES;
FLOG(#"store successfully migrated");
[self deregisterForStoreChanges];
_persistentStoreCoordinator = nil;
_managedObjectContext = nil;
self.storeURL = [self icloudStoreURL];
// Now delete the local file
if (shouldDelete) {
FLOG(#" deleting local store");
[self deleteLocalStore];
} else {
FLOG(#" not deleting local store");
}
return TRUE;
}
else {
FLOG(#"Failed to migrate store: %#, %#", error, error.userInfo);
return FALSE;
}
}
return FALSE;
}
You move your existing store to a different path, and then you call the migrate method with the toURL set to the path where you want your store to end up.
You need to pass the options in that include the ubiquity settings that an iCloud store needs to have set.
When the migration is finished, you should have two copies of the store: the non-iCloud one which you moved aside, and the new one with iCloud options set. You can now remove the old store if you like, and just setup your Core Data stack to use the iCloud store.
Take a look at some of the methods in this example. In particular, look at the ones beginning with 'migrate'. You should be able to work out what steps to take to migrate data to a new cloud store.
Core Data sync is hard to get right, especially when you start to get into migrating in data. It is worth looking at other Core Data sync options like Wasabi Sync and Ensembles. They handle migration and merging of data automatically. (Disclosure: I develop Ensembles)

Core Data custom metadata not saving for iCloud store

I am trying to save some custom metadata to an iCloud-enabled Core Data SQLite store. The documentation appears to say that in addition to the metadata it generates, I should be able to set my own custom metadata - similar to NSUserPreferences, but attached to my Core Data store. It seems like it should be simple:
NSManagedObjectContext* moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[moc setPersistentStoreCoordinator:psc];
NSMutableDictionary* metadata = [[psc metadataForPersistentStore:store] mutableCopy];
metadata[#"testKey"] = #"testValue";
[psc setMetadata:metadata forPersistentStore:store];
[moc save:&error];
So after this runs my custom metadata key/value is visible in the store's metadata field. However, when I restart the app the metadata I added is no longer there (but the iCloud metadata is there). Note that this works correctly when using a regular non-iCloud store so it appears to be iCloud specific.
Additional information:
So lets say I get the metadata, add my custom fields and then save it using the static methods:
[NSPersistentStoreCoordinator setMetadata:metadata forPersistentStoreOfType:NSSQLiteStoreType URL:self.coreDataController.iCloudStore.URL error:&error];
NSDictionary* reviewMetadata = [NSPersistentStoreCoordinator metadataForPersistentStoreOfType:NSSQLiteStoreType URL:self.coreDataController.iCloudStore.URL error:&error];
The reviewMetadata variable contains the metadata with my added fields. However once I restart the app (and consequently reload the store) it's back to the way it was. I even tried setting a breakpoint and killing the app right after executing the above statements and when I restart the app my custom metadata is still gone.
Any ideas? Am I using metadata incorrectly? Does this not work with iCloud-enabled stores?
This seems to work for me but this is running on OS X. Have not tried the same on iOS.
- (void)setiCloudMetaDataForStore:(NSURL*)fileURL ofType:(NSString *)fileType iCloud:(bool)iCloudEnabled ubiquityName:(NSString*)ubiquityName {
//LOG(#"setiCloudMetaDataForStore called...");
NSError *error;
NSDictionary *metaData = [NSPersistentStoreCoordinator metadataForPersistentStoreOfType:fileType URL:fileURL error:&error];
if (!metaData) {
FLOG(#" problem getting metaData");
FLOG(#" - error is %#, %#", error, error.userInfo);
}
//FLOG(#" metaData is %#", metaData);
//Now see if we can add a setting to indicate it is iCloud enabled
NSMutableDictionary *dict = [[NSMutableDictionary alloc] initWithDictionary:metaData];
[dict setObject:[[NSNumber alloc] initWithBool:iCloudEnabled] forKey:#"iCloudEnabled"];
if (!iCloudEnabled)
[dict removeObjectForKey:NSPersistentStoreUbiquitousContentNameKey];
else
[dict setObject:ubiquityName forKey:NSPersistentStoreUbiquitousContentNameKey];
bool result = [NSPersistentStoreCoordinator setMetadata:dict forPersistentStoreOfType:fileType URL:fileURL error:&error];
if (!result) {
FLOG(#" problem setting metaData");
FLOG(#" - error is %#, %#", error, error.userInfo);
}
return;
}
I see that changes to metadata are (sometimes) kept in the -wal and -shm files, that apparently serve to speed up sqlite. I did not find another way to get my metadata changes in the .sqlite file than to add this option while opening the sqlite store:
NSDictionary *options = #{NSSQLitePragmasOption:#{#"journal_mode":#"DELETE"}};
Using this option the -wal and -shm file are not created, so everything must be committed to the .sqlite file.

What should the logic for first run, internet available be?

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.

Resources