I have an application that first loads some data into an UIManagedDocument, then executes saveToURL:forSaveOperation:completionHandler:. Inside the completionHandler block, it does an update of various elements of this database, and when it's done, it does another saving.
Besides that, the app has 3 buttons that reload the data, re-update the data, and delete one entity of the database, respectively. In every button method, the last instruction is a saving as well.
When I run all this in the simulator, all goes smoothly. But in the device doesn't. It constantly crashes. I have observed that, normally, it crashes when pressing the "delete" button, or when reloading or re-updating the database. And it's always in the saveToURL operation.
In my opinion, the problem comes when there are multiple threads saving the database. As the device executes the code slower, maybe multiple savings come at same time and the app can't handle them correctly. Also, sometimes the delete button doesn't delete the entity, and says that doesn't exist (when it does).
I'm totally puzzled with this, and all this saving operations must be done...In fact, if I remove them, the app behaves even more incoherently.
Any suggestions of what could I do to resolve this problem? Thank you very much!
[Edit] Here I post the problematic code. For first loading the data, I use a helper class, with this two methods in particular:
+ (void)loadDataIntoDatabase:(UIManagedDocument *)database
{
[database.managedObjectContext performBlock:^{
// Read from de plist file and fill the database
[database saveToURL:database.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:^(BOOL success) {
[DataHelper completeDataOfDatabase:database];
}];
}
+ (void)completeDataOfDatabase:(UIManagedDocument *)database
{
[database.managedObjectContext performBlock:^{
// Read from another plist file and update some parameters of the already existent data (uses NSFetchRequest and works well)
// [database saveToURL:database.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:nil];
[database updateChangeCount:UIDocumentChangeDone];
}];
}
And in the view, I have 3 action methods, like these:
- (IBAction)deleteButton {
[self.database.managedObjectContext performBlock:^{
NSManagedObject *results = ;// The item to delete
[self.database.managedObjectContext deleteObject:results];
// [self.database saveToURL:self.database.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:NULL];
[self.database updateChangeCount:UIDocumentChangeDone];
}];
}
- (IBAction)reloadExtraDataButton {
[DataHelper loadDataIntoDatabase:self.database];
// [self.database saveToURL:self.database.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:NULL];
[self.database updateChangeCount:UIDocumentChangeDone];
}
- (IBAction)refreshDataButton {
[DataHelper completeDataOfDatabase:self.database];
//[self.database saveToURL:self.database.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:NULL];
[self.database updateChangeCount:UIDocumentChangeDone];
}
[Edit 2] More code: First of all, the initial view executes viewDidLoad this way:
- (void)viewDidLoad{
[super viewDidLoad];
self.database = [DataHelper openDatabaseAndUseBlock:^{
[self setupFetchedResultsController];
}];
}
This is what the setupFetchedResultsController method looks like:
- (void)setupFetchedResultsController
{
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"Some entity name"];
request.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:#"name" ascending:YES selector:#selector(localizedCaseInsensitiveCompare:)]];
self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request
managedObjectContext:self.database.managedObjectContext
sectionNameKeyPath:nil
cacheName:nil];
}
Each view of the app (it has tabs) has a different setupFetchedResultsController in order to show the different entities the database contains.
Now, in the helper class, this is the first class method that gets executed, via the viewDidLoad of each view:
+ (UIManagedDocument *)openDatabaseAndUseBlock:(completion_block_t)completionBlock
{
NSURL *url = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
url = [url URLByAppendingPathComponent:#"Database"];
UIManagedDocument *database = [[UIManagedDocument alloc] initWithFileURL:url];
if (![[NSFileManager defaultManager] fileExistsAtPath:[database.fileURL path]]) {
[database saveToURL:database.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
[self loadDataIntoDatabase:database];
completionBlock();
}];
} else if (database.documentState == UIDocumentStateClosed) {
// Existe, pero cerrado -> Abrir
[database openWithCompletionHandler:^(BOOL success) {
[self loadDataIntoDatabase:database];
completionBlock();
}];
} else if (database.documentState == UIDocumentStateNormal) {
[self loadDataIntoDatabase:database];
completionBlock();
}
return database;
}
You didn't really provide much code. The only real clue you gave was that you are using multiple threads.
UIManagedDocument has two ManagedObjectContexts (one specified for the main queue, and the other for a private queue), but they still must each only be accessed from within their own thread.
Thus, you must only use managedDocument.managedObjectContext from within the main thread. If you want to use it from another thread, you have to use either performBlock or performBlockAndWait. Similarly, you can never know you are running on the private thread for the parent context, so if you want to do something specifically to the parent, you must use performBlock*.
Finally, you really should not be calling saveToURL, except when you initially create the database. UIManagedDocument will auto-save (in its own time).
If you want to encourage it to save earlier, you can send it updateChangeCount: UIDocumentChangeDone to tell it that it has changes that need to be saved.
EDIT
You should only call saveToURL when you create the file for the very first time. With UIManagedDocument, there is no need to call it again (and it can actually cause some unintended issues).
Basically, when you create the document DO NOT set your iVar until the completion handler executes. Otherwise, you could be using a document in a partial state. In this case, use a helper, like this, in the completion handler.
- (void)_document:(UIManagedDocument*)doc canBeUsed:(BOOL)canBeUsed
{
dispatch_async(dispatch_get_main_queue(), ^{
if (canBeUsed) {
_document = doc;
// Now, the document is ready.
// Fire off a notification, or notify a delegate, and do whatever you
// want... you really should not use the document until it's ready, but
// as long as you leave it nil until it is ready any access will
// just correctly do nothing.
} else {
_document = nil;
// Do whatever you want if the document can not be used.
// Unfortunately, there is no way to get the actual error unless
// you subclass UIManagedDocument and override handleError
}
}];
}
And to initialize your document, something like...
- (id)initializeDocumentWithFileURL:(NSURL *)url
{
if (!url) {
url = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
url = [url URLByAppendingPathComponent:#"Default_Project_Database"];
}
UIManagedDocument *doc = [[UIManagedDocument alloc] initWithFileURL:url];
if (![[NSFileManager defaultManager] fileExistsAtPath:[doc.fileURL path]]) {
// The file does not exist, so we need to create it at the proper URL
[doc saveToURL:doc.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
[self _document:doc canBeUsed:success];
}];
} else if (doc.documentState == UIDocumentStateClosed) {
[doc openWithCompletionHandler:^(BOOL success) {
[self _document:doc canBeUsed:success];
}];
} else {
// You only need this if you allow a UIManagedDocument to be passed
// in to this object -- in which case the code above that initializes
// the <doc> variable will be conditional on what was passed...
BOOL success = doc.documentState == UIDocumentStateNormal;
[self _document:doc canBeUsed:success];
}
}
The "pattern" above is necessary to make sure you do not use the document until it is fully ready for use. Now, that piece of code should be the only time you call saveToURL.
Note that by definition, the document.managedObjectContext is of type NSMainQueueConcurrencyType. Thus, if you know your code is running on the main thread (like all your UI callbacks), you do not have to use performBlock.
However, if you are actually doing loads in the background, consider..
- (void)backgroundLoadDataIntoDocument:(UIManagedDocument*)document
{
NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
moc.parentContext = document.managedObjectContext;
[moc performBlock:^{
// Do your loading in here, and shove everything into the local MOC.
// If you are loading a lot of stuff from the 'net (or elsewhere),
// consider doing it in strides, so you deliver objects to the document
// a little at a time instead of all at the end.
// When ready to save, call save on this MOC. It will shove the data up
// into the MOC of the document.
NSrror *error = nil;
if ([moc save:&error]) {
// Probably don't have to synchronize calling updateChangeCount, but I do it anyway...
[document.managedObjectContext performBlockAndWait:^{
[document updateChangeCount:UIDocumentChangeDone];
}];
} else {
// Handle error
}
}];
}
Instead of parenting your background MOC to the mainMOC, you can parent it to the parentContext. Loading and then saving into it will put the changes "above" the main MOC. The main MOC will see those changes the next time it does a fetch operation (note the properties of NSFetchRequest).
NOTE: Some people have reported (and it also appears as a note in Erica Sadun's book), that after the very first saveToURL, you need to close, then open to get everything working right.
EDIT
This is getting really long. If you had more points, I'd suggest a chat. Actually, we can't do it through SO, but we could do it via another medium. I'll try to be brief, but please go back and reread what I posted, and pay careful attention because your code is still violating several tenants.
First, in viewDidLoad(), you are directly assigning your document to the result of calling openDatabaseAndUseBlock. The document is not in a usable state at that time. You do not want the document accessible until the completion handlers fire, which will not happen before openDatabaseAndUseBlock() returns.
Second, only call saveToURL the very first time you create your database (inside openDatabaseAndUseBlock()). Do not use it anywhere else.
Third. Register with the notification center to receive all events (just log them). This will greatly assist your debugging, because you can see what's happening.
Fourth, subclass UIManagedDocument, and override the handleError, and see if it is being called... it's the only way you will see the exact NSError if/when it happens.
3/4 are mainly to help you debug, not necessary for your production code.
I have an appointment, so have to stop now. However, address those issues, and here's on
Related
I'm experimenting with Firebase's FDataSnapshot to pull in data and I would like it to write its data to my core data using MagicalRecord.
According to Firebases "best practice" blog I need to keep a reference to the "handle" so it can be cleaned up later on. Further, they mention to put the FDSnapshot code in viewWillAppear.
I am wanting a callback so that when its finished doing its thing to update core data.
But I'm really note sure how to do that; its doing two things and giving a return at the same time.
// In viewWillAppear:
__block NSManagedObjectContext *context = [NSManagedObjectContext MR_context];
self.handle = [self.ref observeEventType:FEventTypeValue withBlock:^(FDataSnapshot *snapshot) {
if (snapshot.value == [NSNull null])
{
NSLog(#"Cannot find any data");
}
else
{
NSArray *snapshotArray = [snapshot value];
// cleanup to prevent duplicates
[FCFighter MR_truncateAllInContext:context];
for (NSDictionary *dict in snapshotArray)
{
FCFighter *fighter = [FCFighter insertInManagedObjectContext:context];
fighter.name = dict[#"name"];
[context MR_saveToPersistentStoreWithCompletion:^(BOOL contextDidSave, NSError *error){
if (error)
{
NSLog(#"Error - %#", error.localizedDescription);
}
}];
}
}
}];
NSFetchRequest *fr = [[NSFetchRequest alloc] initWithEntityName:[FCFighter entityName]];
fr.sortDescriptors = #[[NSSortDescriptor sortDescriptorWithKey:#"name" ascending:YES]];
self.fighterList = (NSArray *) [context executeFetchRequest:fr error:nil];
[self.tableView reloadData];
In the above code, the core data reading does not wait for the firebase to complete.
Thus, my query -- how would I best combine a completion handler so that when it is complete to update core data, and reload the tableview.
Many thanks
This is a common issue when working with Asynchronous data.
The bottom line is that all processing of data returned from an async call (in this case, the snapshot) needs to be done inside the block.
Anything done outside the block may happen before the data is returned.
So some sudo code
observeEvent withBlock { snapshot
//it is here where snapshot is valid. Process it.
NSLog(#"%#", snapshot.value)
}
Oh, and a side note. You really only need to track the handle reference when you are going to do something else with it later. Other than that, you can ignore the handles.
So this is perfectly valid:
[self.ref observeEventType:FEventTypeValue withBlock:^(FDataSnapshot *snapshot) {
//load your array of tableView data from snapshot
// and/or store it in CoreData
//reload your tableview
}
When opening a locked file using my NSPersistentDocument subclass I get the following message in the console:
Attempt to add read-only file at path [URL] read/write. Adding
it read-only instead. This will be a hard error in the future; you
must specify the NSReadOnlyPersistentStoreOption.
The document window title is '(document name) - Locked'. After the user unlocks it, makes a change and then attempts to save, the save fails with the error
An error occurred while saving.
It seems that NSPersistentDocument fails to recognize that the user has unlocked the document and doesn't reopen it in read/write mode. Is this a bug in NSPersistentDocument or am I missing something here?
I am not overriding any of the file I/O methods in NSPersistentDocument.
Ah, ok automatic file locking.
That happens for auto-save documents not accessed in a while.
The typical approach is to notice the lock before creating the core data stack and put up a dialog asking the user to unlock the file.
If they agree to unlock the file, you simply unlock it and run as normal.
If they don't agree to unlock it, you copy it or open it readonly. Of course, you could simply bypass the user's preference and automatically unlock the file anyway, but that's probably not very nice.
Here is a category that should help you determine if a file is locked, and also lock/unlock the file.
Note, that this is entirely separate from the files mode being changed to read-only, but you can handle it in a similar manner.
Category interface
#interface NSFileManager (MyFileLocking)
- (BOOL)isFileLockedAtPath:(NSString *)path;
- (BOOL)unlockFileAtPath:(NSString*)path error:(NSError**)error;
- (BOOL)lockFileAtPath:(NSString*)path error:(NSError**)error;
#end
Category implementation
#implementation NSFileManager (MyFileLocking)
- (BOOL)isFileLockedAtPath:(NSString *)path {
return [[[self attributesOfItemAtPath:path error:NULL]
objectForKey:NSFileImmutable] boolValue];
}
- (BOOL)unlockFileAtPath:(NSString*)path error:(NSError**)error {
return [self setAttributes:#{NSFileImmutable:#NO}
ofItemAtPath:path
error:error];
}
- (BOOL)lockFileAtPath:(NSString*)path error:(NSError**)error {
return [self setAttributes:#{NSFileImmutable:#YES}
ofItemAtPath:path
error:error];
}
#end
Then, you can call [[NSFileManager defaultManager] isFileLockedAtPath:path] to determine if it is locked, and if it is, throw up a dialog asking the user what to do about it. You can then unlock it and open the stack as normal, or leave it locked and open the stack read-only, which will prevent saves from changing the file store.
Note that you can also monitor the file, and know when it changes from locked/unlocked and respond accordingly.
For Apple's guidelines on this, see https://developer.apple.com/library/mac/documentation/DataManagement/Conceptual/DocBasedAppProgrammingGuideForOSX/StandardBehaviors/StandardBehaviors.html
EDIT
Ok. I would have liked for NSPersistentDocument to replicate the
behavior in NSDocument - where the prompt to unlock comes only when an
edit is attempted. What you're saying is that there is no such feature
in NSPersistentDocument? – Aderstedt
OK. I thought you were wanting to ask the user to unlock it so that it could be opened read/write.
If you want to "go with the flow" and open it read-only when necessary, then you should add a little customization to your NSPersistentDocument subclass.
First, you want to add a little state to keep track of whether or not the original options specified a read-only file.
#implementation MyDocument {
BOOL explicitReadOnly;
}
Then, you will want a couple of utility methods...
- (NSDictionary*)addReadOnlyOption:(NSDictionary*)options {
NSMutableDictionary *mutable = options ? [options mutableCopy]
: [NSMutableDictionary dictionary];
mutable[NSReadOnlyPersistentStoreOption] = #YES;
return [mutable copy];
}
- (NSDictionary*)removeReadOnlyOption:(NSDictionary*)options {
NSMutableDictionary *mutable = options ? [options mutableCopy]
: [NSMutableDictionary dictionary];
[mutable removeObjectForKey:NSReadOnlyPersistentStoreOption];
return [mutable copy];
}
Next, you want to provide your own persistent store coordinator configuration code. This allows you to provide the read-only option to the store when you create it. This method is automatically called when you build your document, all you need to do is provide an override implementation.
- (BOOL)configurePersistentStoreCoordinatorForURL:(NSURL *)url
ofType:(NSString *)fileType
modelConfiguration:(NSString *)configuration
storeOptions:(NSDictionary<NSString *,id> *)storeOptions
error:(NSError * _Nullable __autoreleasing *)error {
explicitReadOnly = [storeOptions[NSReadOnlyPersistentStoreOption] boolValue];
if (![[NSFileManager defaultManager] isWritableFileAtPath:url.path]) {
storeOptions = [self addReadOnlyOption:storeOptions];
}
return [super configurePersistentStoreCoordinatorForURL:url
ofType:fileType
modelConfiguration:configuration
storeOptions:storeOptions
error:error];
}
Also, notice that NSPersistentDocument implements the NSFilePresenter protocol. Thus, you can override a method and be notified whenever the file content or attributes are changed. This will notify you for any change to the file, including lock/unlock from within your application, the Finder, or any other mechanism.
- (void)presentedItemDidChange {
[self ensureReadOnlyConsistency];
[super presentedItemDidChange];
}
We then want to ensure that our persistent store remains consistent with the read-only properties of the file.
Here is one implementation, that just changes the store's readOnly property.
- (void)ensureReadOnlyConsistency {
NSURL *url = [self presentedItemURL];
BOOL fileIsReadOnly = ![[NSFileManager defaultManager] isWritableFileAtPath:url.path];
NSPersistentStoreCoordinator *psc = self.managedObjectContext.persistentStoreCoordinator;
[psc performBlock:^{
NSPersistentStore *store = [psc persistentStoreForURL:url];
if (store) {
if (fileIsReadOnly) {
if (!store.isReadOnly) {
store.readOnly = YES;
}
} else if (!explicitReadOnly) {
if (store.isReadOnly) {
store.readOnly = NO;
}
}
}
}];
}
This works, but has one little hangup. If the store is originally opened with read-only options, then the very first time the readOnly attribute is set to NO, that first save throws (actually, it's the obtainPermanentIDsForObjects:error: call. Core data appears to catch the exception, but it is logged to the console.
The save continues, and nothing seems amiss. All the objects get saved, and the object IDs are properly obtained and recorded as well.
So, there is nothing that does not work that I can tell.
However, there is another more draconian option, but it avoids the aforementioned "issue." You can replace the store.
- (void)ensureReadOnlyConsistency {
NSURL *url = [self presentedItemURL];
BOOL fileIsReadOnly = ![[NSFileManager defaultManager] isWritableFileAtPath:url.path];
NSPersistentStoreCoordinator *psc = self.managedObjectContext.persistentStoreCoordinator;
[psc performBlock:^{
NSPersistentStore *store = [psc persistentStoreForURL:url];
if (store) {
if (fileIsReadOnly != store.isReadOnly) {
NSString *type = store.type;
NSString *configuration = store.configurationName;
NSDictionary *options = store.options;
if (fileIsReadOnly) {
options = [self addReadOnlyOption:options];
} else if (!explicitReadOnly) {
options = [self removeReadOnlyOption:options];
}
NSError *error;
if (![psc removePersistentStore:store error:&error] ||
![psc addPersistentStoreWithType:type
configuration:configuration
URL:url
options:options
error:&error]) {
// Handle the error
}
}
}
}];
}
Finally, note that the notification happens when the operating system notices that the file has changed. When the file is locked/unlocked from within your application, you can get a faster notification.
You can override these two methods to get a little quicker response to the change...
- (void)lockWithCompletionHandler:(void (^)(NSError * _Nullable))completionHandler {
[super lockWithCompletionHandler:^(NSError * _Nullable error) {
if (completionHandler) completionHandler(error);
if (!error) [self ensureReadOnlyConsistency];
}];
}
- (void)unlockWithCompletionHandler:(void (^)(NSError * _Nullable))completionHandler {
[super unlockWithCompletionHandler:^(NSError * _Nullable error) {
if (completionHandler) completionHandler(error);
if (!error) [self ensureReadOnlyConsistency];
}];
}
I hope that's what you are looking for.
I've been experimenting with CLLocationManager's startMonitoringSignificantLocationChanges and I've run into some problems with Core Data. It turns out that since iOS 5.0, Core Data defaults to using NSFileProtectionCompleteUntilFirstUserAuthentication. This means that if a passcode is set, the persistent store is unavailable from the time the device is turned on until the time the passcode is first entered. If you're using location updates, it's possible your app may get launched during that time, and Core Data will get an error trying to load the persistent store.
Obviously switching to NSFileProtectionNone would be the easiest way to solve this. I'd prefer not to though—I'm not storing anything super sensitive in the database, but these location updates aren't super critical either.
I know I can use [[UIApplication sharedApplication] isProtectedDataAvailable] to check whether the data has been unlocked yet, and I can use applicationProtectedDataWillBecomeUnavailable: in my application delegate to respond appropriately once it is unlocked. This seems messy to me though—I'll have to add in a bunch of extra checks to make sure nothing goes wrong if the persistent store is unavailable, re-setup a bunch of things once it does become available, and so on. And that extra code doesn't offer much benefit—the app still won't be able to do anything if it launches in this state.
So I guess I'm just not sure which is the more "proper" way to deal this:
Switch to NSFileProtectionNone.
Add in the extra checks to skip over things if the store is unavailable, and use applicationProtectedDataWillBecomeUnavailable: to set things up again once it is.
If the app is launched in the background ([[UIApplication sharedApplication] applicationState] == UIApplicationStateBackground) and protected data is unavailable ([[UIApplication sharedApplication] isProtectedDataAvailable] == NO)) just call exit(0) (or something similar) to quit the app. On one hand this seems like the simplest solution, and I don't really see any downsides. But it also seems… "wrong"? I guess I can't decide if it's a clean solution or just a lazy one.
Something else I'm just not thinking of?
After thinking this over for a while I've come up with a solution I'm happy with. One thing to consider with the exit(0) option is that if the user takes a while to unlock the device, the app could be continually loading, quitting, and reloading. Whereas if you simply prevent the app from doing much, it will probably only have to load once, and will most likely be more efficient. So I decided to try my option 3 and see how messy it really was. It turned out to be simpler than I thought.
First I added a BOOL setupComplete property to my app delegate. This gives me an easy way to check if the app was fully launched at various points. Then in application:didFinishLaunchingWithOptions: I attempt to initialize the managed object context, then do something like this:
NSManagedObjectContext *moc = [self managedObjectContext];
if (moc) {
self.setupComplete = YES;
[self setupWithManagedObjectContext:moc];
} else {
UIApplication *app = [UIApplication sharedApplication];
if ([app applicationState] == UIApplicationStateBackground && ![app isProtectedDataAvailable]) {
[app beginIgnoringInteractionEvents];
} else [self presentErrorWithTitle:#"There was an error opening the database."];
}
setupWithManagedObjectContext: is just a custom method that finishes setting up. I'm not sure the beginIgnoringInteractionEvents is necessary, but I added it to be on the safe side. That way when the app is brought to the front, I can be sure the interface is frozen until setup is complete. It might avoid a crash if an eager user is tapping anxiously.
Then in applicationProtectedDataDidBecomeAvailable: I call something like this:
if (!self.setupComplete) {
NSManagedObjectContext *moc = [self managedObjectContext];
if (moc) {
self.setupComplete = YES;
[self setupWithManagedObjectContext:moc];
UIApplication *app = [UIApplication sharedApplication];
if ([app isIgnoringInteractionEvents]) [app endIgnoringInteractionEvents];
} else [self presentErrorWithTitle:#"There was an error opening the database."];
}
That finishes the setup and re-enables the interface. That's most of the work, but you'll also need to check through your other code to make sure nothing that relies on Core Data is getting called before your persistent store is available. One thing to watch out for is that applicationWillEnterForeground and applicationDidBecomeActive may get called before applicationProtectedDataDidBecomeAvailable if the user launches the app from this background state. So in various places I've added if (self.setupComplete) { … } to make sure nothing runs before it's ready. I also had a couple of places where I needed to refresh the interface once the database was loaded.
In order to (partially) test this without a lot of driving around, I temporarily modified application:didFinishLaunchingWithOptions: to not set up the database:
NSManagedObjectContext *moc = nil; // [self managedObjectContext];
if (moc) {
self.setupComplete = YES;
[self setupWithManagedObjectContext:moc];
} else {
UIApplication *app = [UIApplication sharedApplication];
// if ([app applicationState] == UIApplicationStateBackground && ![app isProtectedDataAvailable]) {
[app beginIgnoringInteractionEvents];
// } else [self presentErrorWithTitle:#"There was an error opening the database."];
}
Then I moved my code in applicationProtectedDataDidBecomeAvailable: over to applicationWillEnterForeground:. That way I could launch the app, make sure nothing unexpected happens, press the home button, open the app again, and make sure everything was working. Since the actual code requires moving a significant distance and waiting five minutes each time, this gave me a good way to approximate what was happening.
One last thing that tripped me up was my persistent store coordinator. A typical implementation might look something like this:
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
if (_persistentStoreCoordinator != nil) return _persistentStoreCoordinator;
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:#"Test.sqlite"];
NSError *error = nil;
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
}
return _persistentStoreCoordinator;
}
This is loosely based on Apple's sample code, which does explain in comments that you need to handle the error appropriately. My own code does a bit more than this, but one thing I hadn't considered is that if there's an error loading the persistent store, this will return a non-nil result! That was allowing all my other code to proceed as though it was working correctly. And even if persistentStoreCoordinator was called again, it would just return the same coordinator, without a valid store, instead of trying to load the store again. There are various ways you could deal with this, but to me it seemed best to not set _persistentStoreCoordinator unless it was able to add the store:
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
if (_persistentStoreCoordinator != nil) return _persistentStoreCoordinator;
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:#"Test.sqlite"];
NSError *error = nil;
NSPersistentStoreCoordinator *coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
if ([coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
_persistentStoreCoordinator = coordinator;
} else {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
}
return _persistentStoreCoordinator;
}
I have experienced, that you have to check
[[UIApplication sharedApplication] isProtectedDataAvailable]
and process
applicationProtectedDataWillBecomeUnavailable
to be sure you don't access a protected file.
Checking for
managedObjectContext
did not work for me.
My app creates an object (PFUSER) for each user, and an (PF) object for each event they participate in. This works fine. then i have two files associated with that event. i save the first file to a PFFile, then associate it to the event pfobject. when i use blocks and do this in the background, how can then make sure control continues to do the same for the second file?
I am new to blocks so maybe it would be clearer to me why its not working with callbacks, but it seems the block runs the save in another thread and the current one is abandoned before the next steps are taken.
Of course i'd like to do both of these as "save eventually" to allow offline use.
any guidance / examples you can point me to greatly appreciated.
thanks!
saveEventually doesn't support PFFiles yet; it needs a bit more smarts to handle resuming uploads between restarts. One trick that is already available, however, is that PFObject knows how to save its children, including PFFiles. You can just say:
PFUser *user = PFUser.currentUser;
user[#"icon"] = [PFFile fileWithData:iconData];
user[#"iconThumb"] = [PFFile fileWithData:iconThumbData];
[user saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
// user will automatically save its files & only call this once the
// entire operation succeeds.
}];
I'm not 100% what you mean because you didn't post any codes, but I'd imagine if you want to associate multiple PFFile to PFObject this is all you have to do:
PFObject *object = [PFQuery getObjectOfClass:#"MyFile" objectId:id];
[object addObject:profilePicture forKey:#"Photo"];
[object addObject:coverPicture forKey:#"PhotoCover"];
[object saveEventually];
From Parse's documentation it seems like saveEventually does what you want:
Saves this object to the server at some unspecified time in the
future, even if Parse is currently inaccessible. Use this when you may
not have a solid network connection, and don’t need to know when the
save completes. If there is some problem with the object such that it
can’t be saved, it will be silently discarded. If the save completes
successfully while the object is still in memory, then callback will
be called.
As currently neither saveEvetually nor saving to the local data store are supported, below is a category of PFObject I am using to at least save offline what can be saved or returning error:
- (void) dr_saveWithCompletionHandler: (void(^)(NSError* error)) completionBlock {
__block BOOL canSaveEventually = YES;
[[self allKeys] enumerateObjectsUsingBlock:^(NSString* key, NSUInteger idx, BOOL *stop) {
id object = self[key];
if ([object isKindOfClass:[PFFile class]]) {
PFFile* file = (PFFile*) object;
if (!file.url || file.isDirty) {
canSaveEventually = NO;
}
}
}];
void (^localCompletionHandler) (BOOL, NSError*) = ^(BOOL succeeded, NSError *error) {
if (succeeded) {
if (completionBlock) completionBlock(nil);
} else {
if (completionBlock) completionBlock(error);
}
};
if (canSaveEventually) {
[self saveEventually:localCompletionHandler];
} else {
[self saveInBackgroundWithBlock:localCompletionHandler];
}
}
This is my first question on Stack Overflow, so please excuse me if I'm breaking any etiquette. I'm also fairly new to Objective-C/app creation.
I have been following the CS193P Stanford course, in particular, the CoreData lectures/demos. In Paul Hegarty's Photomania app, he starts with a table view, and populates the data in the background, without any interruption to the UI flow. I have been creating an application which lists businesses in the local area (from an api that returns JSON data).
I have created the categories as per Paul's photo/photographer classes. The creation of the classes themselves is not an issue, it's where they are being created.
A simplified data structure:
- Section
- Sub-section
- business
- business
- business
- business
- business
- business
My application starts with a UIViewController with several buttons, each of which opens a tableview for the corresponding section (these all work fine, I'm trying to provide enough information so that my question makes sense). I call a helper method to create/open the URL for the UIManagedDocument, which was based on this question. This is called as soon as the application runs, and it loads up quickly.
I have a method very similar to Paul's fetchFlickrDataIntoDocument:
-(void)refreshBusinessesInDocument:(UIManagedDocument *)document
{
dispatch_queue_t refreshBusinessQ = dispatch_queue_create("Refresh Business Listing", NULL);
dispatch_async(refreshBusinessQ, ^{
// Get latest business listing
myFunctions *myFunctions = [[myFunctions alloc] init];
NSArray *businesses = [myFunctions arrayOfBusinesses];
// Run IN document's thread
[document.managedObjectContext performBlock:^{
// Loop through new businesses and insert
for (NSDictionary *businessData in businesses) {
[Business businessWithJSONInfo:businessData inManageObjectContext:document.managedObjectContext];
}
// Explicitly save the document.
[document saveToURL:document.fileURL
forSaveOperation:UIDocumentSaveForOverwriting
completionHandler:^(BOOL success){
if (!success) {
NSLog(#"Document save failed");
}
}];
NSLog(#"Inserted Businesses");
}];
});
dispatch_release(refreshBusinessQ);
}
[myFunctions arrayOfBusinesses] just parses the JSON data and returns an NSArray containing individual businessses.
I have run the code with an NSLog at the start and end of the business creation code. Each business is assigned a section, takes 0.006 seconds to create, and there are several hundred of these. The insert ends up taking about 2 seconds.
The Helper Method is here:
// The following typedef has been defined in the .h file
// typedef void (^completion_block_t)(UIManagedDocument *document);
#implementation ManagedDocumentHelper
+(void)openDocument:(NSString *)documentName UsingBlock:(completion_block_t)completionBlock
{
// Get URL for document -> "<Documents directory>/<documentName>"
NSURL *url = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
url = [url URLByAppendingPathComponent:documentName];
// Attempt retrieval of existing document
UIManagedDocument *doc = [managedDocumentDictionary objectForKey:documentName];
// If no UIManagedDocument, create
if (!doc)
{
// Create with document at URL
doc = [[UIManagedDocument alloc] initWithFileURL:url];
// Save in managedDocumentDictionary
[managedDocumentDictionary setObject:doc forKey:documentName];
}
// If the document exists on disk
if ([[NSFileManager defaultManager] fileExistsAtPath:[url path]])
{
[doc openWithCompletionHandler:^(BOOL success)
{
// Run completion block
completionBlock(doc);
} ];
}
else
{
// Save temporary document to documents directory
[doc saveToURL:url
forSaveOperation:UIDocumentSaveForCreating
completionHandler:^(BOOL success)
{
// Run compeltion block
completionBlock(doc);
}];
}
}
And is called in viewDidLoad:
if (!self.lgtbDatabase) {
[ManagedDocumentHelper openDocument:#"DefaultLGTBDatabase" UsingBlock:^(UIManagedDocument *document){
[self useDocument:document];
}];
}
useDocument just sets self.document to the provided document.
I would like to alter this code to so that the data is inserted in another thread, and the user can still click a button to view a section, without the data import hanging the UI.
Any help would be appreciated I have worked on this issue for a couple of days and not been able to solve it, even with the other similar questions on here. If there's any other information you require, please let me know!
Thank you
EDIT:
So far this question has received one down vote. If there is a way I could improve this question, or someone knows of a question I've not been able to find, could you please comment as to how or where? If there is another reason you are downvoting, please let me know, as I'm not able to understand the negativity, and would love to learn how to contribute better.
There are a couple of ways to this.
Since you are using UIManagedDocument you could take advantage of NSPrivateQueueConcurrencyType for initialize a new NSManagedObjectContext and use performBlock to do your stuff. For example:
// create a context with a private queue so access happens on a separate thread.
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
// insert this context into the current context hierarchy
context.parentContext = parentContext;
// execute the block on the queue of the context
context.performBlock:^{
// do your stuff (e.g. a long import operation)
// save the context here
// with parent/child contexts saving a context push the changes out of the current context
NSError* error = nil;
[context save:&error];
}];
When you save from the context, data of the private context are pushed to the current context. The saving is only visible in memory, so you need to access the main context (the one linked to the UIDocument) and do a save there (take a look at does-a-core-data-parent-managedobjectcontext-need-to-share-a-concurrency-type-wi).
The other way (my favourite one) is to create a NSOperation subclass and do stuff there. For example, declare a NSOperation subclass like the following:
//.h
#interface MyOperation : NSOperation
- (id)initWithDocument:(UIManagedDocument*)document;
#end
//.m
#interface MyOperation()
#property (nonatomic, weak) UIManagedDocument *document;
#end
- (id)initWithDocument:(UIManagedDocument*)doc;
{
if (!(self = [super init])) return nil;
[self setDocument:doc];
return self;
}
- (void)main
{
NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] init];
[moc setParentContext:[[self document] managedObjectContext]];
// do the long stuff here...
NSError *error = nil;
[moc save:&error];
NSManagedObjectContext *mainMOC = [[self document] managedObjectContext];
[mainMOC performBlock:^{
NSError *error = nil;
[mainMOC save:&error];
}];
// maybe you want to notify the main thread you have finished to import data, if you post a notification remember to deal with it in the main thread...
}
Now in the main thread you can provide that operation to a queue like the following:
MyOperation *op = [[MyOperation alloc] initWithDocument:[self document]];
[[self someQueue] addOperation:op];
P.S. You cannot start an async operation in the main method of a NSOperation. When the main finishes, delegates linked with that operations will not be called. To say the the truth you can but this involves to deal with run loop or concurrent behaviour.
Hope that helps.
Initially I was just going to leave a comment, but I guess I don't have the privileges for it. I just wanted to point out the UIDocument, beyond the change count offers
- (void)autosaveWithCompletionHandler:(void (^)(BOOL success))completionHandler
Which shouldn't have the delay I've experienced with updating the change count as it waits for a "convenient moment".