How to synchronise two NSManagedObjectContext - iphone

I'm working on an ipad application that use coredata. It download information on a database that is on the web, and record them in coredata. The application is based on a split view. My problem was to make the download and the record of the data in background. Here is how I've done :
- I've create an NSOperation, that does the download and the record of the data.
- This NSOperation use a different NSManagedObjectContext than the context of the appDelegate, return by this function, that is in the appDelegate :
(NSManagedObjectContext*)newContextToMainStore {
NSPersistentStoreCoordinator *coord = nil;
coord = [self persistentStoreCoordinator];
NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] init];
[moc setPersistentStoreCoordinator:coord];
return [moc autorelease];
}
- I've had an observer in the NSOperation, that will call this function in the appDelegate when I save the context, to modify the context of the delegate too :
- (void)mergeChangesFromContextSaveNotification:(NSNotification*)notification {
[[self managedObjectContext]mergeChangesFromContextDidSaveNotification:notification];
}
But I've a problem, the synchronisation doesn't work, because the data on the rootViewController (that is a UITableViewController), that have a NSManagedObjectContext initialised with the context of the appDelegate and use as datasource a NSFetchedResultsController, don't actualise automatically the informations, as it normaly must do.
So I ask you : What did I do wrong ? Is it the good way to use two different context and synchonise them ?

What you have here looks correct. You do want to make sure you implement the NSFetchedResultControllerDelegate methods in the rootViewController so the changes will appear in the UI.

Related

How to ensure NSManagedObjectContext when opened asynchronously through UIManagedDocument

I have an application with different controllers that all operate on the same NSManagedObjectContext.
My approach was to initialize the NSManagedObjectContext in my AppDelegate and inject it into all the controllers.
I am initializing my NSManagedObjectContext by opening a UIManagedDocument like this:
UIManagedDocument* databaseDoc = [[UIManagedDocument alloc] initWithFileURL:url];
if (![[NSFileManager defaultManager] fileExistsAtPath:[databaseDoc.fileURL path]]) {
[databaseDoc saveToURL:databaseDoc.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
myController.managedObjectContext = databaseDoc.managedObjectContext;
}];
} else if (databaseDoc.documentState == UIDocumentStateClosed) {
[databaseDoc openWithCompletionHandler:^(BOOL success) {
myController.managedObjectContext = databaseDoc.managedObjectContext;
}];
} else if (databaseDoc.documentState == UIDocumentStateNormal){
myController.managedObjectContext = databaseDoc.managedObjectContext;
}
Now my problem is, that opening the UIManagedDocument happens asynchronously and the NSManagedObjectContext is only available in the completion block.
How do I ensure that the controllers always have a valid NSManagedObjectContext to work with? Of course the problems happen at startup i.e. when a controller wants to use the NSManagedObjectContext in his "viewDidLoad" method, and the completion block in the AppDelegate has not yet run ...
One approach would probably be to "wait" in the AppDelegate until the UIDocument has opened, but as far as I gather this is not recommended ...
I would like to avoid to "pollute" my controllers with code that deals with the asynchronous nature of opening a NSManagedObjectContext... but maybe this is a naive wish?
In your appDelegate:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
MyWaitViewController* waitController = [[MyWaitViewController new] autorelease];
self.window.rootViewController = waitController;
// then somewheres else, when you get your context
[databaseDoc saveToURL:databaseDoc.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
myContextController.managedObjectContext = databaseDoc.managedObjectContext;
self.window.rootViewController = myContextController;
// note that at this point when the viewDidLoad method will get called
// it will have his managedObjectContext and his view already available.
// you can change your rootController, or push another viewController into the
// stack. Depending on what u want from the GUI side
}];
return YES;
}
Note that you dispose the GUI logic into the MyWaitViewController + AppDelegate side. But you keep your "myContextController" away from that logic control, since he get called / created only when a context exist.
I was struggling with the same issue, and I came up with it by using NSNotificationCenter.
When initializing your NSManagedObjectContext in the success handler, add send a notification.
Then, add a listener to to the viewDidLoad of whatever your first ViewController is.
I used that listener to call a reloadData method. In a heavy app, this could be a problem, as the viewcontroller loads blank, and then reloads the data, but this is a lite one, and it's noticeable at all - the viewController loads instantaneously with the managedObjectContext.

Passing Objects Between Classes

In my AppDelegate, I download some data from the internet and store it into an array. I want one of my ViewControllers to access that array. How would I go about in doing so? Is this a good situation to implement a delegate or a protocol? If so, can someone recommend a good tutorial for that?
Thank you
EDIT:
Please note that the data refreshes upon each launch so there is no need for Core Data or plists. Furthermore, the data are custom objects which I created so they can't be stored in a plist for example.
You have 2 options:
Implement a delegate protocol
Use NSNotifications
The advantages/disadvantages of each is set out well in this question and answer:
Delegates v Notifications
As notifications are easier to implement and may well be sufficient for your needs, you can implement it with the following steps:
In the class where you download the data:
When the data has been downloaded and the array populated, include the following lines:
NSDictionary *dict = [NSDictionary dictionaryWithObject:array forKey:#"Data"];
[[NSNotificationCenter defaultCenter] postNotificationName:#"DataDownloaded" object:self userInfo:dict];
In the class where you want to receive the data:
2.1 Add the following line to your viewDidLoad method:
`[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(dataDownloaded:) name:#"DataDownloaded" object:nil];
2.2 Create the dataDownloaded selector:
(void)dataDownloaded:(NSNotification *)note {
NSDictionary *dict = note.userInfo;
NSArray *dataArray = [note.userInfo objectForKey:#"DataDownloaded"];
2.3 Add the following line to dealloc and viewDidUnload:
[[[NSNotificationCenter defaultCenter] removeObserver:self];
You can store data in plist file and use it in all view controllers. This way, you need not worry about the size of data (as you will load it on demand and free it immediately).
if you want to store your array in delegate then in any view you have to create reference of your delegate and you can access your array in that view like :
in your other view you have to write : in .h file
#import "YourDelegateFile.h" and declare a varialble
YourDelegateFile *appDelegate ;
in your .m file :
- (void) viewDidLoad
{
appDelegate = (YourDelegateFile *)[UIApplication sharedApplication] delegate];
NSArray *aArray = [appDelegate yourArrayName]; //yourArrayName is an array that you have declare in delegate
}
hope it helps you.
you just need to access the data stored within the appdelegate. I dont think this is the best solution to your problem but in order to do things the way you want.
so declare you property in your Appdelegate .h file
NSMutableArray* myArray_;
then add a property to the same file
#property (nonatomic, retain) NSMutableArray* myArray;
in the .m file
make sure you synthesize your property
#synthesize myArray = myArray_;
somewhere in your appdelegate .m file you will set the value
then, elsewhere in your code you can access the property in the appdelegate like so
MyAppDelegate *appDelegate = (MyAppDelegate *)[UIApplication sharedApplication].delegate
NSMutableArray* localArray = appDelegate.myArray;
Note, for good encapsulation you should really use an NSArray but i used mutable to keep the code short.
Also, using the appdelegate as a global store for program data is not a good idea, it breaks a lot of rules you shouldnt break, single responsibility principle being a good one to start with. You should ideally be storing application data in a dedicated class, perhaps a singleton or for much better testability a single instance class served by a factory class. This way you data is accessible from a known well defined entity, it is testable and it honours good design principles
You can send notification if app delegate got new data and all interested controllers will know that they need to update views. For this you can use NSNotificationCenter. For example
- (void)newDataLoaded {
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:arrayOfData forKey:#"data"];
[[NSNotificationCenter defaultCenter] postNotificationName:#"data updated notification name" object:nil userInfo:userInfo];
}
If some controller interested in data updates it should subscribe for this notification as soon as possible:
- (void)viewDidLoad {
...
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(dataUpdatedNotificationHandler:) name:#"data updated notification name" object:nil];
...
}
Do not forget to unsubscribe from notifications if you don't need it. For this use [[NSNotificationCenter defautCenter] removeObserver:self] in viewDidUnload and dealloc methods.
- (void)dataUpdatedNotificationHandler:(NSNotification*)notification {
NSArray *data = [[notification userInfo] objectForKey:#"data"];
// update your view here
}

Generic approach to NSManagedObjectContext in multi-threaded application

I've read a number of posts here about NSManagedObjectContext and multi-threaded applications. I've also gone over the CoreDataBooks example to understand how separate threads require their own NSManagedObjectContext, and how a save operation gets merged with the main NSManagedObjectContext. I found the example to be good, but also too application specific. I'm trying to generalize this, and wonder if my approach is sound.
My approach is to have a generic function for fetching the NSManagedObjectContext for the current thread. The function returns the NSManagedObjectContext for the main thread, but will create a new one (or fetch it from a cache) if called from within a different thread. That goes as follows:
+(NSManagedObjectContext *)managedObjectContext {
MyAppDelegate *delegate = (MyAppDelegate *)[[UIApplication sharedApplication] delegate];
NSManagedObjectContext *moc = delegate.managedObjectContext;
NSThread *thread = [NSThread currentThread];
if ([thread isMainThread]) {
return moc;
}
// a key to cache the context for the given thread
NSString *threadKey = [NSString stringWithFormat:#"%p", thread];
// delegate.managedObjectContexts is a mutable dictionary in the app delegate
NSMutableDictionary *managedObjectContexts = delegate.managedObjectContexts;
if ( [managedObjectContexts objectForKey:threadKey] == nil ) {
// create a context for this thread
NSManagedObjectContext *threadContext = [[[NSManagedObjectContext alloc] init] autorelease];
[threadContext setPersistentStoreCoordinator:[moc persistentStoreCoordinator]];
// cache the context for this thread
[managedObjectContexts setObject:threadContext forKey:threadKey];
}
return [managedObjectContexts objectForKey:threadKey];
}
Save operations are simple if called from the main thread. Save operations called from other threads require merging within the main thread. For that I have a generic commit function:
+(void)commit {
// get the moc for this thread
NSManagedObjectContext *moc = [self managedObjectContext];
NSThread *thread = [NSThread currentThread];
if ([thread isMainThread] == NO) {
// only observe notifications other than the main thread
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(contextDidSave:)
name:NSManagedObjectContextDidSaveNotification
object:moc];
}
NSError *error;
if (![moc save:&error]) {
// fail
}
if ([thread isMainThread] == NO) {
[[NSNotificationCenter defaultCenter] removeObserver:self
name:NSManagedObjectContextDidSaveNotification
object:moc];
}
}
In the contextDidSave: function we perform the merge, if called by the notification in commit.
+(void)contextDidSave:(NSNotification*)saveNotification {
MyAppDelegate *delegate = (MyAppDelegate *)[[UIApplication sharedApplication] delegate];
NSManagedObjectContext *moc = delegate.managedObjectContext;
[moc performSelectorOnMainThread:#selector(mergeChangesFromContextDidSaveNotification:)
withObject:saveNotification
waitUntilDone:YES];
}
Finally, we clean-up the cache of NSManagedObjectContext with this:
+(void)initialize {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(threadExit)
name:NSThreadWillExitNotification
object:nil];
}
+(void)threadExit {
MyAppDelegate *delegate = (MyAppDelegate *)[[UIApplication sharedApplication] delegate];
NSString *threadKey = [NSString stringWithFormat:#"%p", [NSThread currentThread]];
NSMutableDictionary *managedObjectContexts = delegate.managedObjectContexts;
[managedObjectContexts removeObjectForKey:threadKey];
}
This compiles and seems to work, but I know threading problems can be tricky due to race conditions. Does anybody see a problem with this approach?
Also, I'm using this from within the context of an asynchronous request (using ASIHTTPRequest), which fetches some data from a server and updates and inserts the store on the iPhone. It seems NSThreadWillExitNotification doesn't fire after the request completes, and the same thread is then used for subsequent requests. This means the same NSManagedObjectContext is used for separate requests on the same thread. Is this a problem?
A year after posting this question I finally built a framework to generalize and simplify my working with Core Data. It goes beyond the original question, and adds a number of features to make Core Data interactions much easier. Details here: https://github.com/chriscdn/RHManagedObject
I found a solution after finally understanding the problem better. My solution doesn't directly address the question above, but does address the problem of why I had to deal with threads in the first place.
My application uses the ASIHTTPRequest library for asynchronous requests. I fetch some data from the server, and use the delegate requestFinished function to add/modify/delete my core-data objects. The requestFinished function was running in a different thread, and I assumed this was a natural side-effect of asynchronous requests.
After digging deeper I found that ASIHTTPRequest deliberately runs the request in a separate thread, but can be overridden in my subclass of ASIHTTPRequest:
+(NSThread *)threadForRequest:(ASIHTTPRequest *)request {
return [NSThread mainThread];
}
This small change puts requestFinished in the main thread, which has eliminated my need to care about threads in my application.

CoreData weird behavior when data are loaded on background thread

I have very strange problem, when I don't understand what's going on at all, so I'm looking for explanation of it. Situation is as following:
I have a view controller with scrollview with three subviews in it. Those three subviews have method
-(void)loadContent
which loads content from database using CoreData in the background thread, creates subviews which represent loaded items and add them as own subviews calling [self addSubview: itemView]; That method is invoked as
[self performSelectorInBackground: #selector(loadContent) withObject: nil];
To load data from DB I'm using a singleton service class. Everything worked fine, but when those three views are loading their portions of data, it sometimes crashes the app.
I guessed it's because it shares one NSManagedObjectContext instance for all read operations, so I rewrote the class so it shares only NSManagedObjectModel and NSPersistentStoreCoordinator instances and creates it's own NSManagedObjectContext instance.
Suddenly, very strange thing happened. Data are loaded ok, subviews are created and added to the view hierarchy, but it get never displayed on the screen. When I switch back to the old singleton service class (sharing one managedObjectContext), it works again like a charm! (but with risk of crashing the app, though).
I absolutely don't get the point how loading data from DB is related to displaying items on the screen. More on that - when subviews are created and added to the view hierarchy, why the hell it don't get displayed?
The source looks like this:
- (void) loadContent {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSArray *results = [(WLDataService *)[WLDataService service] loadItemsForGDView];
NSUInteger channelPosition = 0;
CGFloat position = 0.0;
CGFloat minuteWidth = ((self.superview.frame.size.width / 2.0) / 60.0);
for(Item *it in results) {
/// On following lines size and position of the view is computed according to item setup - skipping here...
/// Create item; it's simple subclass of UIView class
WLGDItemView *item = [[WLGDItemView alloc] init];
/// Variables used here are declared above when size and position is computed
item.frame = CGRectMake(itemX, itemY, itemWidth, itemHeight);
[self performSelectorOnMainThread: #selector(addSubview:) withObject: item waitUntilDone: NO];
/// This is just helper macro to release things
WL_RELEASE_SAFELY(item);
}
[pool drain];
}
The basic service class (non-singleton one) implementation is as follows (just interesting parts):
#import "WLLocalService.h"
static NSPersistentStoreCoordinator *sharedPSC = nil;
static NSManagedObjectModel *sharedMOM = nil;
#implementation WLLocalService
#synthesize managedObjectContext;
/// This is here for backward compatibility reasons
+ (WLLocalService *) service {
return [[[self alloc] init] autorelease];
}
#pragma mark -
#pragma mark Core Data stack
- (NSManagedObjectContext *) managedObjectContext {
if (managedObjectContext == nil) {
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil) {
managedObjectContext = [[NSManagedObjectContext alloc] init];
[managedObjectContext setPersistentStoreCoordinator: coordinator];
}
[managedObjectContext setUndoManager: nil];
[managedObjectContext setMergePolicy: NSMergeByPropertyStoreTrumpMergePolicy];
}
return managedObjectContext;
}
- (NSManagedObjectModel *) managedObjectModel {
if(sharedMOM == nil) {
sharedMOM = [[NSManagedObjectModel mergedModelFromBundles: nil] retain];
}
return sharedMOM;
}
- (NSPersistentStoreCoordinator *) persistentStoreCoordinator {
if(sharedPSC == nil) {
NSURL *storeUrl = [self dataStorePath];
NSError *error = nil;
sharedPSC = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: [self managedObjectModel]];
if (![sharedPSC addPersistentStoreWithType: NSSQLiteStoreType configuration: nil URL: storeUrl options: nil error: &error]) {
WLLOG(#"%#: %#", error, [error userInfo]);
}
}
return sharedPSC;
}
#pragma mark -
#pragma mark Path to data store file
- (NSURL *) dataStorePath {
return [NSURL fileURLWithPath: [WL_DOCUMENTS_DIR() stringByAppendingPathComponent: #"/DB.sqlite"]];
}
- (void)dealloc {
WL_RELEASE_SAFELY(managedObjectModel);
[super dealloc];
}
#end
I'd really love to know what's going on here and why it behaves so strange (and - of course - why it does not work, in particular). Can anybody explain that?
thanks to all
Have you read Multi Threading with Core-Data twice?
First, do not load or construct UI elements on a background thread. The UI (whether on the desktop or on the iPhone) is single threaded and manipulating it on multiple threads is a very bad idea.
Second, data that you load into one context will not be immediately visible in another context. This is what is causing part of your problem.
The solution is to move all your UI code to the main thread and warm up the Core Data cache on a background thread. This means to load the data on a background thread (into a separate cache) to load it into the NSPersistentStoreCoordinator cache. Once that is complete your main thread can access that data very quickly because it is now in memory.
You realize that [WLDataService service] does not actually return a singleton? It creates a new instance every time. So you are effectively working with multiple instances of the Core Data components.
What about:
static WLDataService* gSharedService = NULL;
#implementation WLDataService
+ (id) service
{
#synchronized (self) {
if (gSharedService == NULL) {
gSharedService = [[self alloc] init];
}
}
return gSharedService;
}
#end
That will create the same instance every time. You will also want to make your managedObjectContext, managedObjectModel and persistentStoreCoordinator methods thread safe by using a #synchronized block. Otherwise there is a change that multiple threads will initialize those at the same time, leading to unexpected behaviour.

Undo Management with Core Data

I'm trying to implement undo support using Core Data on the iPhone and I ran into a few problems.
I currently have a couple of managed objects set up but when I make changes to their properties, these changes don't get recorded by the undo manager. From my understanding, Core Data is supposed to have this automatically set up and I should be able to have basic undo and redo support for changes, creation and deletion of managed objects.
Is there special way of making changes to the objects so that they get recorded by the undo manager? Or should I be registering undo actions for each change?
Also, suppose the application slides into a detailed view for editing a specific object. I would like to be able to undo all changes made when say, the cancel button is hit. Would undo grouping be applicable here? What is the difference between committing a group and have another undo manager manage the finer actions in the detailed view versus using just having one undo manager (the one included with the managed object context)?
Thanks!
While the undo features will work pretty much out of the box, you do need to allocate an NSUndoManager for the NSManagedObjectContext for which you want undo support.
The easiest way to do this is to set up the undo support when something asks your appDelegate for the NSManagedObjectContext
This is the default method that apple gives you:
- (NSManagedObjectContext *) managedObjectContext {
if (managedObjectContext != nil) {
return managedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil) {
managedObjectContext = [[NSManagedObjectContext alloc] init];
[managedObjectContext setPersistentStoreCoordinator: coordinator];
}
return managedObjectContext;
}
Modify it to look like this:
- (NSManagedObjectContext *) managedObjectContext {
if (managedObjectContext != nil) {
return managedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil) {
managedObjectContext = [[NSManagedObjectContext alloc] init];
//Undo Support
NSUndoManager *anUndoManager = [[NSUndoManager alloc] init];
[managedObjectContext setUndoManager:anUndoManager];
[anUndoManager release];
[managedObjectContext setPersistentStoreCoordinator: coordinator];
}
return managedObjectContext;
}