I am writing my first comprehensive app using Core Data, and I want to see what the best way to keep track of various object changes / updates / deletes is. For example, I have a Notes entity and a Location entity, and a one-to-one relationship between them, the idea being that each note could have its location tagged. I then have a UITableView with a fetchedResultsController driving the list of notes (where you can add new notes and attach a date and location to them), but then I have 2 other view controllers, one with a map view and one with a calendar view. The map view fetches all the locations in Location and displays them on a map. The calendar view basically gets all the data from Notes again and just shows it in a calendar view.
How should I keep track of changes to Notes and Location in my calendar and map view? It's easy to load them up once in viewDidLoad, but how should I keep track of all the changes, so that when the user revisits the mapview (for e.g.) he/she sees the latest data as well.
The one way I've deciphered is to listen for notifications in NSManagedObjectContextObjectsDidChangeNotification, in both the maps view and the calendar view. This seems to return all the inserted, deleted and updated objects from a managed context, each time there's a save. I could then go through these objects and see if I need to update my view. This is how I'm thinking of doing it:
In MapViewController viewDidLoad:
[[NSNotificationCenter defaultCenter]
addObserver: self
selector: #selector(objectChangedNotificationReceived:)
name: NSManagedObjectContextObjectsDidChangeNotification
object: context];
Then:
- (void) objectChangedNotificationReceived: (NSNotification *) notification
{
NSArray* insertedObjects = [[notification userInfo]
objectForKey:NSInsertedObjectsKey] ;
NSArray* deletedObjects = [[notification userInfo]
objectForKey:NSDeletedObjectsKey] ;
NSArray* updatedObjects = [[notification userInfo]
objectForKey:NSUpdatedObjectsKey] ;
NSLog(#"insertObjects: %#", [insertedObjects description]);
NSLog(#"deletedObjects: %#", [deletedObjects description]);
NSLog(#"updatedObjects: %#", [updatedObjects description]);
for (NSManagedObject *obj in insertedObjects) {
if ([obj class] == [Location class]) {
NSLog(#"adding a new location");
Location *locationObj = (Location *) obj;
[self.mapview addAnnotation: locationObj];
}
}
}
Does this seem about right? It seems like a lot of redundant code to put into each view controller, especially if I'm interested in more than one NSManagedObject. Is there some other technique that I'm missing?
NSFetchedResultsController seems to fit your requirement. It will efficiently manage the data handling from the Core Data. You reuse the same fetch request for both of your calendar view controller and map view controller.
Why can't you just fetch the latest data directly from CoreData on the viewWillLoad or viewDidLoad method. This will ensure that you have the latest objects.
Seems like that would be more modular and cleaner.
Related
I have a basic UITableView where I can add items using Core Data as well as delete if needed.
Let's say I add 5 items to the UITableView. I want to also display these five items on another view, preferably a PDF.
What's the best approach to accomplish this?
I'm attempting using NSUserDefaults as well, but the only value that is appearing is the last value that is entered.
Here's some of the main code used. Any advice is appreciated!
This is from the UIViewController where I input the text:
-(NSManagedObjectContext * )managedObjectContext
{
return [(AppDelegate *) [[UIApplication sharedApplication] delegate] managedObjectContext];
}
-(void)saveButtonTapped:(id)sender
{
[self.managedObjectContext save:nil];
if (_majorTextField.text == nil)
{
_majorString = #"";
}
else
{
_majorString = [[NSString alloc] initWithFormat:#"%#", _majorTextField.text];
self.task.title = self.majorTextField.text;
NSUserDefaults * majorDefault = [NSUserDefaults standardUserDefaults];
[majorDefault setObject:_majorString forKey:#"major"];
}
[self.navigationController popViewControllerAnimated:YES];
}
Here is where I am adding the text on my PDF:
+(void)createPDF:(NSString*)filePath
{
// Create the PDF context using the default page size of 612 x 792.
UIGraphicsBeginPDFContextToFile(filePath, CGRectZero, nil);
// Mark the beginning of a new page.
UIGraphicsBeginPDFPageWithInfo(CGRectMake(0, 0, 612, 792), nil);
NSUserDefaults * majorDefault = [NSUserDefaults standardUserDefaults];
NSString * majorString = [majorDefault stringForKey:#"major"];
[PDFRenderer drawText:majorString inFrame:CGRectMake(35, 190, 300, 50) fontName:#"TimesNewRomanPSMT" fontSize:14];
UIGraphicsEndPDFContext();
}
You are saving your NSManagedObjectContext before you make changes to the represented object in your view controller. In saveButtonTapped: you save the context but only later set self.task.title ... Perhaps I'm missing something; but I think you want save the context after changing self.task.title.
In the other views that need to access the managed objects in question, you need to fetch them by constructing an NSFetchRequest and executing that request against an NSManagedObjectContext. I can't give any substantial example without knowing more about your model; but that's the basic idea.
Depending on your needs, Core Data may be ideal or it may be overkill. There's a lot to be gained by using Core Data for your object graph persistence technology - but there's a lot of subtlety. As Apple puts it. "Core Data is not an entry level technology." (Apple, "Getting Started with Core Data")
We're probably all guilty of this, but you should pass an error object in [self.managedObjectContext save:nil]; and handle the return value, thusly:
NSError *saveError = nil;
if( ![[self managedObjectContext] save:&saveError] ) {
// do something with saveError
}
EDIT:
Core Data is fine for what you describe in your comment. It's just a matter of understanding the underlying principles of persistent stores, managed object contexts, etc. What you do get with Core Data are a set of conveniences for displaying data in table views, i.e. NSFetchedResultsController.
I'm working on the data import part in my app, and to make the UI more reliable, i followed this Marcus Zarra article
http://www.cimgf.com/2011/08/22/importing-and-displaying-large-data-sets-in-core-data/
The idea is that you make the import in a separate context in the background tread(i use GCD for that), and your fetchedResultsController's context merges the changes by observing the NSManagedObjectContextDidSaveNotification.
The issue i get is very strange to me - my fetchedResultsController doesn't get those changes itsef and doesn't reload the TableView when the new data comes.
But if i fire the following method, which makes the fetch and reloads the table - it gets it all there.
- (void)updateUI
{
NSError *error;
if (![[self fetchedResultsController] performFetch:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
}
[self.tableView reloadData];
}
So now i call that method when i get the NSManagedObjectContextDidSaveNotification to make it work, but it looks strange and nasty to me.
- (void)contextChanged:(NSNotification*)notification
{
if ([notification object] == [self managedObjectContext]) return;
if (![NSThread isMainThread]) {
[self performSelectorOnMainThread:#selector(contextChanged:) withObject:notification waitUntilDone:NO];
return;
}
[[self managedObjectContext] mergeChangesFromContextDidSaveNotification:notification];
//TODO:Make it work as it should - merge, without updateUI
[self updateUI];//!!!Want to get rid of this!
}
Why can it be like this?
Here is the code that is responsible for parsing the data and adding the Observer.
- (void)parseWordsFromServer:(NSNotification *)notification
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0) , ^{
NSDictionary *listInJSON = [notification userInfo];
wordsNumbers = [[listInJSON valueForKey:#"words"]mutableCopy];
if ([wordsNumbers count])
{
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(contextChanged:) name:NSManagedObjectContextDidSaveNotification object:nil];
//New Context for the new thread
NSManagedObjectContext *backContext = [[AppDelegate sharedAppDelegate]backManagedObjectContext];
//Get all the words we already have on this device
NSArray *wordsWeHave = [Word wordsWithNumbers:wordsNumbers inManagedContext:backContext];
//Add them to this list
for (Word *word in wordsWeHave)
[[List listWithID:[currentList listID] inManagedObjectContext:backContext]addWordsObject:word];
[backContext save:nil];!//Save the context - get the notification
}
});
}
EDIT
I use the NSFetchedResutsControllerDelegate, indeed, how else could i pretend my tableview to be updated if i didn't?
UPDATE Decided just to move to Parent - Child paradigm
The problem has been discussed many times, like NSFetchedResultsController doesn't show updates from a different context, and it's quite difficult to understand what is going on but I have few notes.
First, you are violating a simple rule: you need to have a managed object context per thread (Concurrency with Core Data Section).
Create a separate managed object context for each thread and share a
single persistent store coordinator.
So, inside your custom thread access the main context, grab its persistent coordinator and set it to the new context.
NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] init];
[moc setPersistentStoreCoordinator:persistentStoreCoordinatorGrabbedFromAppDelegate];
Second, you don't need to register
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(contextChanged:) name:NSManagedObjectContextDidSaveNotification object:nil];
within the new thread. Just register for it within the class that create the new thread (or in the app delegate).
Finally, if you are not using a NSFetchedResutsControllerDelegate, use it. It allows to get rid of reloading data table. When the context changes, the delegate responds to changes: edit, remove, add.
Starting from iOS 5, you could just use new Core Data API and make your life easier with new confinement mechanism.
Edit
From #mros comment.
Multi-Context CoreData
It may help you understand a little bit more about the advantages of
using a parent-child core data model. I particularly like the bit
about using a private queue context to handle the persistent store.
Make sure to read down through the whole thing because the beginning
shows how not to do it.
Hope that helps.
Background:
In my app, I'm specifically targeting Mac OS X Lion. This issue involves Core Data, an NSPopover and a child NSManagedObjectContext (created by using the new parentContext property of NSManagedObjectContext).
I have a table of NSManagedObjects of class "Location". There's an Add button that calls addLocation: and if a table row is double-clicked, I call tableViewDoubleClick:.
For either case, what I do is create a new NSManagedObjectContext and set its parent context to that of the document's context. I then either create a new Location in that context or fetch the Location to be edited from the temporary context. I set the popover's representedObject property to the location in question. If I cancel the popover, nothing is saved. If the user clicks a Save button in the popover, I just call save: on the temporary context and the changes get pushed to the main context.
addLocation:
- (IBAction)addLocation:(id)sender
{
LocationEditViewController *popupController = [[[LocationEditViewController alloc] init] autorelease];
popupController.title = #"Add New Location";
NSManagedObjectContext *tempContext = [[[NSManagedObjectContext alloc] init] autorelease];
tempContext.parentContext = self.document.managedObjectContext;
Location *tempLocation = [NSEntityDescription insertNewObjectForEntityForName:#"Location" inManagedObjectContext:tempContext];
popupController.representedObject = tempLocation;
popupController.managedObjectContext = tempContext;
[popupController.popover showRelativeToRect:[sender bounds] ofView:sender preferredEdge:NSMaxYEdge];
}
tableViewDoubleClick:
- (void)tableViewDoubleClick:(id)sender
{
NSInteger selectedRow = [self.table selectedRow];
if (selectedRow != -1)
{
NSRect rectOfSelectedRow = [self.table rectOfRow:selectedRow];
LocationEditViewController *popupController = [[[LocationEditViewController alloc] init] autorelease];
popupController.title = #"Edit Location";
Location *locationToEdit = [self.locationController.selectedObjects objectAtIndex:0];
NSManagedObjectContext *tempContext = [[[NSManagedObjectContext alloc] init] autorelease];
tempContext.parentContext = self.document.managedObjectContext;
Location *tempLocation = (Location *)[tempContext fetchObjectEqualTo:locationToEdit]; // Custom fetch helper method
popupController.managedObjectContext = tempContext;
popupController.representedObject = tempLocation;
[popupController.popover showRelativeToRect:rectOfSelectedRow ofView:sender preferredEdge:NSMaxXEdge];
}
}
Here's the problem that I'd like an explanation for:
The text fields in the popover are connected to the popover's representedObject via bindings in the nib. These work perfectly with a new object (addLocation:).
If the Location is an existing object (tableViewDoubleClick:), the bindings work well enough to pre-populate the fields with the Location's properties. However, changing the text in the fields does not alter the Location's properties at all. When the Save button in the popup is clicked, I tried logging the Location's properties before saving the temporary context. If it's an existing object, whatever I type into the fields isn't being reflected in the Location's properties - as if the bindings are only communicating one-way.
My workaround: I found that if I skip the bindings and just manually set the Location's properties to the values in the text fields before the save, that the changes do take effect.
- (IBAction)popoverSave:(id)sender
{
// These two methods always work. But if I remove these and use bindings instead, it only works for NEW Locations.
[(Location *)self.representedObject setLabel:self.labelField.stringValue];
[(Location *)self.representedObject setLocation:self.locationField.stringValue];
NSLog(#"representedObject = %#", self.representedObject);
NSError *error = nil;
[self.managedObjectContext save:&error];
[self.popover close];
}
I'd really like to know why this is the case, just in case I'm actually doing something wrong.
Thanks!
I think it's likely that it is the cast in these lines:
[(Location *)self.representedObject setLabel:self.labelField.stringValue];
[(Location *)self.representedObject setLocation:self.locationField.stringValue];
… that makes them work. If so, then you probably have a NSObject or NSManagedObject set somewhere in the bindings as the class instead of the Location class. When the binding sends a Location class specific message e.g. set an attribute with a specific name, to the generic class, the generic class silently ignores the message.
BTW, I would caution against using multiple context instead of using the undo API. I see a lot of people get in trouble that way. It's easier to roll back a single context than it is to manage multiple context.
In my Iphone application I am trying to navigate from one table view controller to next table view controller. Problem I am facing is that I have to fetch data using http request and then parse this data when the user select a cell. I am able to fetch and parse the data but the view controller is not waiting for the data to parsed and the next view controller is shown (which is empty). How to over come this problem.
indexSelected = [NSString stringWithFormat: #"%d",[indexPath row] ];
[[MySingletonClass sharedMySingleton] doAnAuthenticatedAPIFetch_Subscriber_Detail : indexSelected];
SubscribersDetailViews2 *viewController = [[SubscribersDetailViews2 alloc] initWithNibName:#"SubscribersDetailViews2" bundle:nil];
[[self navigationController] pushViewController:viewController animated:YES];
[viewController release];
This is what you do:
indexSelected = [NSString stringWithFormat: #"%d",[indexPath row] ];
SubscribersDetailViews2 *viewController = [[SubscribersDetailViews2 alloc] initWithNibName:#"SubscribersDetailViews2" bundle:nil];
[[MySingletonClass sharedMySingleton] doAnAuthenticatedAPIFetch_Subscriber_Detail:indexSelected delegate:self];
[[self navigationController] pushViewController:viewController animated:YES];
[viewController release];
You define a protocol that your view controller conforms to and when the fetching and parsing of data is done you call a method on the delegate to let the view controller know that the data is ready to be displayed.
If you need more information on how to do this, leave a comment.
EDIT: So here's how to declare and use a protocol. I'm going to try to keep it as simple as possible. I'm not sure if I like your naming convention, but I'll still use it for this example.
So let's get down to the code. This is how you declare a protocol:
#protocol MySingletonClassDelegate <NSObject>
#optional
- (void)didDoAnAuthenticatedAPIFetch_Subscriber_Detail_WithData:(NSArray *)data;
- (void)failedToDoAnAuthenticatedAPIFetch_Subscriber_Detail_WithError:(NSError *)error;
#end
Again, I'm not too fond of the naming convention. You shouldn't have underscores in objective-c method names.
The protocol should be defined in MySingletonClass.h before the declaration of MySingletonClass.
I declared two methods in the protocol, one for delivering the data and one for delivering an error if it fails, so that you can notify the user that it failed.
To use the protocol you need the following:
#interface SubscribersDetailViews2 : UITableViewController <MySingletonClassDelegate>
You also need to implement the methods declared in the protocol, but I'll leave that implementation to you.
Since the fetching of data already seems to be happening in the background I don't think I'll need to explain how to do that. One important thing to remember is that you want to execute the delegate methods on the main thread. Here's the code to do that:
- (void)doAnAuthenticatedAPIFetch_Subscriber_Detail:(NSUInteger)index delegate:id<MySingletonClassDelegate>delegate {
// Fetching data in background
if (successful) {
[self performSelectorOnMainThread:#selector(didDoAnAuthenticatedAPIFetch_Subscriber_Detail_WithData:) withObject:data waitUntilDone:NO];
} else {
[self performSelectorOnMainThread:#selector(failedToDoAnAuthenticatedAPIFetch_Subscriber_Detail_WithError:) withObject:error waitUntilDone:NO];
}
}
Just to be clear the // Fetching data in background is supposed to be replaced by your code. I assume that your code produces the variables (NSArray *data, NSError *error, BOOL successful) that I use.
That's about it, if you need clarification on anything let me know.
There are a number of options:
Cache the data, i.e., take a full copy of it on the iOS device (may not be practical of course)
Display an interstitial screen saying "loading" and then move to the "real" screen when the data has downloaded
Have, effectively, two different data sources for your table. The first is your current one. The second would be a single cell saying "Loading..."
In short, there's no point and click way of doing this but there's no problem downloading the data on the fly as long as you tell your users what's happening.
After several hours/days of searching and diving into example projects I've concluded that I need to just ask. If I bind the assetsView (IKImageBrowserView) directly to an IB instance of NSArrayController everything works just fine.
- (void) awakeFromNib
{
library = [[NSArrayController alloc] init];
[library setManagedObjectContext:[[NSApp delegate] managedObjectContext]];
[library setEntityName:#"Asset"];
NSLog(#"%#", [library arrangedObjects]);
NSLog(#"%#", [library content]);
[assetsView setDataSource:library];
[assetsView reloadData];
}
Both NSLogs are empty. I know I'm missing something... I just don't know what. The goal is to eventually allow multiple instances of this view's "library" filtered programmatically with a predicate. For now I'm just trying to have it display all of the rows for the "Asset" entity.
Addition: If I create the NSArrayController in IB and then try to log [library arrangedObjects] or manually set the data source for assetsView I get the same empty results. Like I said earlier, if I bind library.arrangedObjects to assetsView.content (IKImageBrowserView) in IB - with same managed object context and same entity name set by IB - everything works as expected.
- (void) awakeFromNib
{
// library = [[NSArrayController alloc] init];
// [library setManagedObjectContext:[[NSApp delegate] managedObjectContext]];
// [library setEntityName:#"Asset"];
NSLog(#"%#", [library arrangedObjects]);
NSLog(#"%#", [library content]);
[assetsView setDataSource:library];
[assetsView reloadData];
}
I was running into a similar situation where the (IKImageBrowserView) was not initializing even though the ArrayController would ultimately sync up with the NSManagedObjectContext.
Ultimately found this passage in the core data programming guide
https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/CoreData/Articles/cdBindings.html#//apple_ref/doc/uid/TP40004194-SW3
if the "automatically prepares content" flag (see, for example,
setAutomaticallyPreparesContent:) is set for a controller, the controller's initial content
is fetched from its managed object context using the controller's current fetch predicate. It
is important to note that the controller's fetch is executed as a delayed operation performed
after its managed object context is set (by nib loading)—this therefore happens after
awakeFromNib and windowControllerDidLoadNib:. This can create a problem if you want to
perform an operation with the contents of an object controller in either of these methods,
since the controller's content is nil. You can work around this by executing the fetch
"manually" with fetchWithRequest:merge:error:.
- (void)windowControllerDidLoadNib:(NSWindowController *) windowController
{
[super windowControllerDidLoadNib:windowController];
NSError *error = nil;
BOOL ok = [arrayController fetchWithRequest:nil merge:NO error:&error];
// ...
It looks like the problem is that you have not actually told the NSArrayController to fetch anything. NSArrayControllers are empty until you add objects either through bindings or manually.
After setting up library try to call its fetch method:
[library fetch:self];
Also, you probably know this already but it is possible to set bindings in code with the following method:
- (void)bind:(NSString *)binding toObject:(id)observableController withKeyPath:(NSString *)keyPath options:(NSDictionary *)options
Can also be added in awakeFromNib if subclassing the NSArrayCotroller or via your view controller
-(void)awakeFromNib
{
[self fetchWithRequest:nil merge:NO error:nil];
...
}