I am creating or opening a UIManagedDocument in my AppDelegate, using completion handler blocks to notify me when the document is ready for use.
// CHECK TO SEE IF MANAGED DOCUMENT ALREADY EXISTS ON DISK
if([fileManager fileExistsAtPath:[documentLocation path]]) {
// EXISTS BUT CLOSED, NEEDS OPENING
[[self managedDocument] openWithCompletionHandler:^(BOOL success) {
NSLog(#"DOCUMENT: Opened ...");
// TODO: Things to do when open.
}];
} else {
//DOES NOT EXIST, NEEDS CREATING AND OPENING
[[self managedDocument] saveToURL:documentLocation forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
NSLog(#"DOCUMENT: Created & Opened ...");
// TODO: Things to do when open.
}];
}
My question is I want to setup a NSFetchedResultsController on my ViewController but currently the controllers view loads before the document (from the AppDelegate) is either created or opened. I am just curious about how I inform the controller that the document is open and ready to use. My guess is I would use a NSNotification, but I just wanted to check I am not going about this the wrong way.
If you have a instance of your ViewController in appDelegate then write a public method in your ViewController and call this method in the block completion handler.
Related
The following method gets called in order to populate my Core-Data after AFNetworking fetches information from my app server.
The information seems to be perfectly working as when the table is updated I can see the new information being updated in the UITableView.
Now the problem that I have is that even tho I can see the information ( after it has been fetches from the server, stored into Core-data and refetches to display in my UITableView) If I then go and close my app and re open it, the information is not there anymore.
It seems as if the information is not persistent and the problem seems to be the thread. given that if I remove the thread option in my method everything works fine.
What am I missing?? I have tried most things that I came across but I can't seem to find a solution.
NSManagedObjectContext *childContext = [[NSManagedObjectContext alloc]
initWithConcurrencyType:NSPrivateQueueConcurrencyType];
childContext.parentContext = managedObjectContext;
myModel.context = childContext;
[childContext performBlock:^{
// ... Lots Controller logic code that then calls the class myModel where all my Core-Data save methods are
// Sort Wall Pictures
if ( [dataHolder[#"verb"] isEqualToString:#"addWallPicture"] ) {
data = #{ #"resourceID":dataHolder[#"_id"][#"$id"],
#"resourceName":dataHolder[#"details"][#"resourceName"],
#"author":#{ #"id":dataHolder[#"userId"][#"$id"],
#"username":dataHolder[#"details"][#"authorName"] },
#"likesNumber":#0,
#"likesPeople":#[]
};
[myModel saveSocialWall:data date:date verb:dataHolder[#"verb"] code:dataHolder[#"_id"][#"$id"] myUser:myUser];
continue;
}
[childContext save:&error];
}];
You have to save the main context as well at some point, e.g. after saving the child context.
Saving the child context saves only to the main context, and saving the main context saves to the store file.
Like this (written on the phone, there will
be syntax errors):
// ...
[childContext save:&error];
[mainContext performBlock:^{
[mainContext save:&error];
}];
In Swift 2.0 that would be:
do {
try childContext.save()
mainContext.performBlock {
do {
try mainContext.save()
} catch let err as NSError {
print("Could not save main context: \(err.localizedDescription)")
}
}
} catch let err as NSError {
print("Could not save private context: \(err.localizedDescription)")
}
Background info
I've almost completed my app. Everything was working perfectly. Then the client asked for logging in the app (i.e. various points that had to record what was done, what responses were, etc...).
The app allows the user to create "messages" which are saved into core-data. The messages are then uploaded to the server individually. The message are created on the main thread and uploaded in an NSOperation subclass on a background thread.
It is the same template for the NSOperation subclass that I have used before and works. I'm doing all the best practise stuff for multi-threaded core-data.
All this side of the app works fine.
I added the logging part of the app. I've created a singleton called MyLogManager and a CoreData entity called LogEntry. The entity is very simple, it only has a date and text.
Code
The function inside the MyLogManager is...
- (void)newLogWithText:(NSString*)text
{
NSLog(#"Logging: %#", text);
NSManagedObjectContext *context = [self context];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"LogEntry" inManagedObjectContext:context];
LogEntry *logEntry = [[LogEntry alloc] initWithEntity:entity insertIntoManagedObjectContext:context];
logEntry.text = text;
logEntry.date = [NSDate date];
[self saveContext:context];
}
which in turn runs...
- (NSManagedObjectContext*)context
{
AppDelegate *appDelegate = (ThapAppDelegate*)[[UIApplication sharedApplication] delegate];
NSManagedObjectContext *managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
[managedObjectContext setPersistentStoreCoordinator:appDelegate.persistentStoreCoordinator];
return managedObjectContext;
}
and
- (void)saveContext:(NSManagedObjectContext*)context
{
MyAppDelegate *appDelegate = (MyAppDelegate*)[[UIApplication sharedApplication] delegate];
[[NSNotificationCenter defaultCenter] addObserver:appDelegate
selector:#selector(mergeChanges:)
name:NSManagedObjectContextDidSaveNotification
object:context];
NSError *error = nil;
if ([context hasChanges] && ![context save:&error]) {
NSLog(#"Unhandled error %#, %#", error, [error userInfo]);
abort();
}
[[NSNotificationCenter defaultCenter] removeObserver:appDelegate name:NSManagedObjectContextDidSaveNotification object:context];
}
The NSOperation main thread (well parts of it)...
- (void)main
{
//create context and retrieve NSManagedObject using the NSManagedObjectID passed in as a parameter to operation
self.message.lastSendAttempt = [NSDate date];
[self startUpload];
[self completeOperation]; //This doesn't get run because the startUpload method never returns
}
- (void)startUpload
{
[[MyLogManager sharedInstance] logSendingMessageWithURLParameters:[self.event URLParameters]]; //this is a convenience method. It just appends some extra info on the string and runs newLogWithText.
//Do some uploading stuff here...
//The operation stops before actually doing the upload when logging to CoreData.
}
The problem
My NSOperation subclass that uploads the messages (on a background thread) calls this newLogWithText function but it also updates the message it is uploading. The NSOperation uses the same methods to get and save the core-data context. (i.e. it updates the last sent date and also updates if the send was successful).
This is the first time I've tried to deal with simultaneous writes and saves to core-data.
I don't get any errors and the app carries on "working". But the operation never completes. I've tried to debug it with breakpoints but when I use breakpoints it works. Without breakpoints the operation never finishes and the upload never happens. And then it just sits there blocking the queue it is on and no other messages can be sent.
In my appDelegate (I know this isn't the ideal place for it but it's the default for a new project and I haven't changed it) the mergeChanges method is just...
- (void)mergeChanges:(NSNotification *)notification
{
[self.managedObjectContext performSelectorOnMainThread:#selector(mergeChangesFromContextDidSaveNotification:) withObject:notification waitUntilDone:NO];
}
I've tried throwing the 'newLogWithText' function off to another thread and even to the main thread with no luck.
I'm just about to try it now but change the "waitUntilDone" of the merge to YES. (Just noticed this). (This didn't work).
I'm 90% certain this is down to simultaneous writes to different contexts and the conflict resolution as it is the first time I've dealt with this. If I comment out the newLogWithText function then everything works as it should.
The only alternative at the moment is to scrap the LogEntry from core data and save the logs into an array inside NSUserDefaults but that doesn't feel right. Is there another way?
EDIT
I've changed it now so it users NSUserDefaults and it works without a problem. It just feels like a hacky solution.
I have an IOS app that uses a core database to store
thumbnail photo images and their user selected rating.
I use multiple managed object contexts for this.
The problem is that when the app is installed and
launched for the first time (and thus creates a new
database) the main MOC does not see updates from the
temporary MOC that is making changes to the photo rating.
However on subsequent launches of the app (i.e. database
exists already), everything works fine each and every
time.
And additionally, on a new app launch, even though the
ratings don't show up in the main viewcontroller, I know
they're being saved to disk, because on a app re-launch
I see the ratings the user had entered.
The main MOC is a list view controller that displays the
photos. When the user selects a photo from the list, it
launches another view controller (with a temporary MOC
tied to the same persistent store) where the user selects
a photo rating. But on a fresh launch of the app, the photo
rating setting never propagates back to the list view controller.
I've included some code. Would appreciate any insights.
Database creation in main listview controller
if ([fileManager fileExistsAtPath:[urlForPhotosDb path]]) {
if (photosDB.documentState == UIDocumentStateClosed) {
[photosDB openWithCompletionHandler:^(BOOL success) {
......(additional code here).........
}];
}
} else {
[photosDB saveToURL:urlForPhotosDb forSaveOperation:UIDocumentSaveForCreating
completionHandler:^(BOOL success) {
[PhotosDatabase populatePhotosDb];
......(additional code here).........
];
}
return photosDB;
}
On View Load in main listView controller
(void)viewDidLoad
{
[super viewDidLoad];
[PhotosDatabase getPhotosDbForOpenBlock:^(UIManagedDocument *doc) {
self.psc = [doc.managedObjectContext persistentStoreCoordinator];
[self setupFetchedResultsController:doc];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(contextSaved:)
name:NSManagedObjectContextDidSaveNotification
object:nil];
}];
}
Setting up of temporary MOC in delegate method of main
listview controller called from secondary view controller
-(void)didSelectPhotoRating:(NSDictionary *)photoInfo Rating:(NSNumber *)rating
{
NSManagedObjectContext *newContext = [[NSManagedObjectContext alloc] init];
[newContext setPersistentStoreCoordinator:self.psc];
Photos *photo = [Photos findPhoto:photoInfo
inManagedObjectContext:newContext];
if (photo) {
photo.rating = rating;
NSError *error;
[newContext save:&error];
}
}
method in main listview controller to merge changes
-(void)contextSaved:(NSNotification *)notification
{
if ([notification object] != self.document.managedObjectContext) {
[self.document.managedObjectContext
mergeChangesFromContextDidSaveNotification:notification];
[self performFetch];
[self.tableView reloadData];
}
}
And in the contextSaved method above, the notification
indeed contains the user selected rating for the photo.
But it doesn't reflect in the main listview controller.
Start using nested contexts. It's just much much much much much easier and in sync on every possible level. The have not updated the core data programming guide to cover it well at this point.
http://www.cocoanetics.com/2012/07/multi-context-coredata/
I have the next issue I'm creating one view when one button is touched. when the view is created and loaded I make 2 request with ASIFormDataRequest one request for download one Image and the other for get some text.
The headache become when the user in the view loaded select back because if button back is pressed the view is removed form superview, but crashh if one request callback is coming and the view dont exist how can I make this like cancel the request or how can I fix that.
The crash is in the next line of code
Class: ASIHTTPRequest.m
BOOL dataWillBeHandledExternally = NO;
**if ([[self delegate] respondsToSelector:[self didReceiveDataSelector]]) {**
dataWillBeHandledExternally = YES;
}
With: Thread 6: EXC_BAD_ACCES (code = 1, address = 0x30047dbc)
Please hellp that has haunted me.
You want to make sure that you cancel any pending ASIHTTPRequest when you pop your view:
From: http://allseeing-i.com/ASIHTTPRequest/How-to-use#cancelling_an_asychronous_request
// Cancels an asynchronous request, clearing all delegates and blocks first
[request clearDelegatesAndCancel];
You can use try catch for it. below is how u can do in ASIHTTPRequest.m
#try {
if ([[self delegate] respondsToSelector:[self didReceiveDataSelector]]) {
dataWillBeHandledExternally = YES;
}
}
#catch (NSException *exception) {
dataWillBeHandledExternally = NO;
}
I have an asset manager that needs to notify the owner it's assets are ready. I'm sending a token back for the consumer to listen to listen for a notification to avoid tighter coupling. The issue is when the assets are already loaded I need to call the loadComplete after a delay. What's the best way to do this in objective-c?
Asset Manager
-(tokenString*) loadAssetPath:(NSString*) asset {
//start asynchronous load
//or if assets ready send complete <-- issue
return nonceToken;
}
-(void)loadComplete {
[[NSNotificationCenter defaultCenter]
postNotificationName:tokenString object:self];
}
Consumer
NSString* token;
-(void) loadSomething {
if(token)
[self removeListener];
token = [[AssetManager sharedManager]
loadAssetPath:#"http://server.dev/myLargeImage.png"];
[[NSNotificationCenter defaultCenter]
addObserver:[AssetManager sharedManager]
selector:#selector(assetLoaded:) name:token];
}
-(void)assetLoader:(NSNotifcation*)aNotification {
[self removeListener];
//continue on with stuffing stuff
}
Use NSObject's performSelector function which allows it to be called after a delay.
- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay
You can even use a form of this function to run it on another thread, which is useful to not blocking the main thread when doing lengthy operations (just don't muck with the UI objects in this thread).
#DavidNeiss is correct about performSelector:withObject:afterDelay:, but you almost certainly don't want an actual time delay here. At most you want to perform your selector on the next event loop, just so things are consistent for the listener. So you should make the delay 0. This differs from the normal performSelect:withObject: which will immediately perform the selector synchronously.
-(tokenString*) loadAssetPath:(NSString*) asset {
//start asynchronous load
if (<load is actually complete>) {
// -loadComplete will execute on the next event loop
[self performSelector:#selector(loadComplete) withObject:nil afterDelay:0];
}
return nonceToken;
}