How do I display my Core Data values? - objective-c

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.

Related

Objective C - Firebase - How to add completion handler to FDataSnapshot

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
}

Fixing delay in Core Data Storage

So I am building in a hide function into my application. In my settings menu I have a UISwitch that should allow the user to hide themselves. I have created the UISwitch's IBAction like so:
-(IBAction)hideUserToggle:(id)sender {
AppDelegate *newAppDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
NSManagedObjectContext *context = [newAppDelegate managedObjectContext];
NSManagedObject *newOwner;
NSEntityDescription *entityDesc = [NSEntityDescription entityForName:#"LoggedInUser" inManagedObjectContext:context];
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:entityDesc];
NSManagedObject *matches = nil;
NSError *error;
NSArray *objects = [context executeFetchRequest:request error:&error];
newOwner = [NSEntityDescription insertNewObjectForEntityForName:#"LoggedInUser" inManagedObjectContext:context];
if (_hideUser.on) {
if ([objects count] == 0) {
NSLog(#"%#",[error localizedDescription]);
} else {
matches = objects[0];
[newOwner setValue:#"userHidden" forKeyPath:#"isHidden"];
NSLog(#"%#",[matches valueForKeyPath:#"isHidden"]);
}
} else {
if([objects count] == 0) {
NSLog(#"%#",[error localizedDescription]);
} else {
matches = objects[0];
[newOwner setValue:#"userNotHidden" forKeyPath:#"isHidden"];
NSLog(#"%#",[matches valueForKeyPath:#"isHidden"]);
}
}
}
This should set the value of the Core Data String that I use to determine whether a person is hidden or not, which I use later in my code as a conditional for loading data. However when I test this feature it doesn't seem to update the persistent data store (Core Data) when the user has flipped the switch. I have looked around everywhere and I found a reference to there being a delay in updating Core Data here -> Why does IOS delay when saving core data via a UIManagedDocument, however it doesn't seem to provide the answer to my problem.
I want to be able flip the switch and save that value so that when the user swipes over to another view controller it is immediately aware that the user has gone into "hiding" or offline so it does not show certain information.
A NSManagedObjectContext is a scratchpad. Changes you make within the context exist only within the context unless or until you save them to the context's parent (either the persistent store itself or another context).
You're not saving them. I'd assume you're therefore not seeing the change elsewhere because you're using different contexts. Meanwhile the change eventually migrates because somebody else happens to save.
See -save: for details on saving.
(aside: the key-value coding [newOwner setValue:#"userHidden" forKeyPath:#"isHidden"]-style mechanism is both uglier and less efficient than using an editor-generated managed object subclass; hopefully it's just there while you're debugging?)

UIManagedDocument insert objects in background thread

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".

Fetching a one-to-many core data relationship returns correct objects the first time but empty set all other times

I have an object, Workout, that has a one-to-many relationship with an object, Exercise.
Diagram of models: http://i.imgur.com/q1Mfq.png
When I create a Workout object, I add three Exercise objects to it by looping over
[self addExercisesObject:exercise]
and then save my managed object context. Then, from my controller for displaying a workout, I can successfully fetch the workout and its exercises (with a fetch request), as shown by the output in my debugger:
Printing description of self->_savedWorkout:
<Workout: 0x6c5a990> (entity: Workout; id: 0x6e46e00 <x-coredata://EA417EAA-101A-4F04-8276-3C4A6CDF094D/Workout/p1> ; data: {
bodyweight = nil;
date = "2012-05-09 16:59:43 +0000";
exercises = (
"0x6e3c870 <x-coredata://EA417EAA-101A-4F04-8276-3C4A6CDF094D/Exercise/p3>",
"0x6e3eaf0 <x-coredata://EA417EAA-101A-4F04-8276-3C4A6CDF094D/Exercise/p2>",
"0x6e36820 <x-coredata://EA417EAA-101A-4F04-8276-3C4A6CDF094D/Exercise/p1>"
);
isCompleted = 0;
workoutId = 1;
workoutPlan = "0x6e6c980 <x-coredata://EA417EAA-101A-4F04-8276-3C4A6CDF094D/WorkoutPlan/p1>";
})
So far so good. However, if I close my app in my simulator and start it up again and perform the same fetch request in same view, the workout looks like this:
Printing description of self->_savedWorkout:
<Workout: 0x6ea8ff0> (entity: Workout; id: 0x6e8f9e0 <x-coredata://EA417EAA-101A-4F04-8276-3C4A6CDF094D/Workout/p1> ; data: {
bodyweight = nil;
date = "2012-05-09 16:59:43 +0000";
exercises = (
);
isCompleted = 0;
workoutId = 1;
workoutPlan = "0x6c8a130 <x-coredata://EA417EAA-101A-4F04-8276-3C4A6CDF094D/WorkoutPlan/p1>";
})
It appears that it fetches the same workout object, but now exercises is an empty set. Actually, exercises first looks like this after the fetch request:
exercises = "<relationship fault: 0x8a93100 'exercises'>";
but once I do:
for (Exercise *exercise in self.savedWorkout.exercises)
self.savedWorkout.exercises resolves to an empty set. I do not edit the workout in anyway in any part of my app.
My fetch request is made by this method in my Workout class:
- (Workout *)getLatestWorkout
{
self.model = [[self.managedObjectContext persistentStoreCoordinator] managedObjectModel];
NSFetchRequest *fetchRequest = [self.model fetchRequestTemplateForName:#"getLatestWorkout"];
NSError *error = nil;
NSArray *results = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
if ([results count] == 1) {
return [results objectAtIndex:0];
}
return nil;
}
I made the fetch request template with Xcode's GUI tool. It fetches all Workout objects where isCompleted == 0. You can see that it fetches the same object each time because the workout's x-coredata path is the same in both debugger outputs.
Update: I checked my SQLite database. There is one workout in the workout table and three exercises in the exercises table.
Any ideas what's going on?
EDIT: Code that creates objects posted below
- (void)storeUserSettings
{
// get the file path if it exists
NSString *path = [[NSBundle mainBundle] pathForResource:#"userSettings" ofType:#"plist"];
// create it if it doesn't
if (path == nil) {
path = [NSString stringWithFormat:#"%#%#",
[[NSBundle mainBundle] bundlePath], #"/userSettings.plist"];
}
// and write the new settings to file
[self.userSettings writeToFile:path atomically:YES];
// load managed object context
[self loadMOC];
WorkoutPlan *currentPlan = [[WorkoutPlan alloc] getActiveWorkoutPlan];
[currentPlan setManagedObjectContext:self.managedObjectContext];
// if user has no plan or is changing plans, create new plan and first workout
if (currentPlan == nil ||
([self.userSettings valueForKey:#"plan"] != currentPlan.planId)) {
// create a workoutPlan object
WorkoutPlan *workoutPlan = [[WorkoutPlan alloc] initWithEntity:
[NSEntityDescription entityForName:#"WorkoutPlan"
inManagedObjectContext:self.managedObjectContext]
insertIntoManagedObjectContext:self.managedObjectContext];
// set attributes to values from userSettings and save object
[workoutPlan createWorkoutPlanWithId:[self.userSettings valueForKey:#"plan"]
schedule:[self.userSettings valueForKey:#"schedule"]
dateStarted:[self.userSettings valueForKey:#"nextDate"]];
}
// if user is just changing schedule, update schedule of current plan
else if (![currentPlan.schedule isEqualToString:[self.userSettings valueForKey:#"schedule"]]) {
[currentPlan setSchedule:[self.userSettings valueForKey:#"schedule"]];
[currentPlan saveMOC];
}
}
- (void)loadMOC
{
AppDelegate *delegate = (AppDelegate*)[[UIApplication sharedApplication] delegate];
self.managedObjectContext = delegate.managedObjectContext;
self.model = [[self.managedObjectContext persistentStoreCoordinator] managedObjectModel];
}
- (void)createWorkoutPlanWithId:(NSNumber *)planId schedule:(NSString *)schedule
dateStarted:(NSDate *)dateStarted
{
[self deactivateCurrentPlan];
// set workout plan attributes
[self setPlanId:planId];
[self setIsActive:[NSNumber numberWithBool:YES]];
[self setSchedule:schedule];
[self setDateStarted:dateStarted];
// create first workout and add to workout plan
Workout *firstWorkout = [NSEntityDescription
insertNewObjectForEntityForName:#"Workout"
inManagedObjectContext:self.managedObjectContext];
[firstWorkout setManagedObjectContext:self.managedObjectContext];
[firstWorkout createFirstWorkoutForPlan:self onDate:dateStarted];
[self addWorkoutsObject:firstWorkout];
[self saveMOC];
}
- (void)createFirstWorkoutForPlan:(WorkoutPlan *)plan onDate:(NSDate *)startDate
{
// set workout attributes
[self setDate:startDate];
[self setIsCompleted:[NSNumber numberWithBool:NO]];
[self setWorkoutId:[NSNumber numberWithInt:1]];
NSArray *exerciseList = [self getExercisesForWorkout:self inPlan:plan];
// iterate over exercises in spec and create them
for (NSDictionary *exerciseSpec in exerciseList)
{
// create a exercise MO
Exercise *exercise = [NSEntityDescription
insertNewObjectForEntityForName:#"Exercise"
inManagedObjectContext:[plan managedObjectContext]];
[exercise setManagedObjectContext:[plan managedObjectContext]];
[exercise createExerciseForWorkout:self withSpec:exerciseSpec];
// add exercise to workout object
[self addExercisesObject:exercise];
}
}
- (void)createExerciseForWorkout:(Workout *)workout withSpec:exerciseSpec
{
// set exercise attributes
self.exerciseId = [exerciseSpec valueForKey:#"id"];
self.isPersonalRecord = [NSNumber numberWithBool:NO];
NSArray *sets = [exerciseSpec valueForKey:#"sets"];
int i = 1;
for (NSNumber *setReps in sets)
{
// create a set MO
Set *set = [NSEntityDescription
insertNewObjectForEntityForName:#"Set"
inManagedObjectContext:[workout managedObjectContext]];
[set setManagedObjectContext:[workout managedObjectContext]];
// set set attributes
set.order = [NSNumber numberWithInt:i];
set.repetitions = setReps;
set.weight = [exerciseSpec valueForKey:#"default_weight"];
// add set to exercise object
[self addSetsObject:set];
i++;
}
}
I had a similar problem. The parent-child relationship worked when the app was running but after re-start only the latest child record was retrieved.
I was adding the children like this:
create the child record
set the child's parent attribute, set the child's other
attributes
add the child to the parent using the parent's add method
I found that it was fixed if I did it like this:
create the child record
add the child to the parent using the parent's add method
set the child's parent attribute, set the child's other
attributes
Core Data is complex. There could be dozens of things to check, any one thing which could be causing issues.
How many MOCs are you using? How are you saving? Many more questions...
I would suggest turning on the SQL debugging flag (-com.apple.CoreData.SQLDebug 1) in the EditScheme for arguments when starting the application.
Run your code, and see what is actually going on.
Relationships resolve to a fault when fetched, unless you override it in the fetch request.
If you are using more than one MOC in a parent/child relationship, the save from the child to the parent just puts data into the parent, it does not really save it. If using UIManagedDocument, it's a whole different set of issues...
I hope this does not sound harsh. Be prepared to provide a whole lot of information for a Core Data question, other than "this is not saving and here is some debugging output."
Basically, how CoreData works depends on how the stack is created, whether using UIManagedDocument or not, multiple threads, how creating objects, how saving them, options on fetch requests, and a whole lot more things.
It's actually not that complex, but there are lots of customizations and special cases depending on how it is used.
EDIT
Post the code that creates the objects/relationships.
Also, try the fetch with a manual fetch request instead of the template. When you look at the data in the database, do you see the foreign keys for the relationships set appropriately?
Run it all again with debugging enabled to see exactly what the SQL is doing. That is more valuable that your own debugging output.
I'm having this exact same problem but my model is pretty complex. My app creates the entities and relationships on startup if they don't already exist. If they are created and I don't exit the app, I'm able to fetch an entity with a to-many relationship and see the correct count of related objects. If I exit my app and restart it (it now knows it doesn't have to create a default set of data) then the relationships are returning a null set. I can't figure it out.
EDIT: I figured out that my problem relates to an Ordered set relation. I had to use a Category to create a work around (found on stack overflow) to insert new entries into an ordered set. So I'm guessing that has something to do with it.

Populating NSImage with data from an asynchronous NSURLConnection

I have hit the proverbial wall trying to figure out how to populate an NSImage with data returned from an asynchronous NSURLConnection in my desktop app (NOT an iPhone application!!).
Here is the situation.
I have a table that is using custom cells. In each custom cell is an NSImage which is being pulled from a web server. In order to populate the image I can do a synchronous request easily:
myThumbnail = [[NSImage alloc] initWithContentsOfFile:myFilePath];
The problem with this is that the table blocks until the images are populated (obviously because it's a synchronous request). On a big table this makes scrolling unbearable, but even just populating the images on the first run can be tedious if they are of any significant size.
So I create an asynchronous request class that will retrieve the data in its own thread as per Apple's documentation. No problem there. I can see the data being pulled and populated (via my log files).
The problem I have is once I have the data, I need a callback into my calling class (the custom table view).
I was under the impression that I could do something like this, but it doesn't work because (I'm assuming) that what my calling class really needs is a delegate:
NSImage * myIMage;
myImage = [myConnectionClass getMyImageMethod];
In my connection class delegate I can see I get the data, I just don't see how to pass it back to the calling class. My connectionDidFinishLoading method is straight from the Apple docs:
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
// do something with the data
// receivedData is declared as a method instance elsewhere
NSLog(#"Succeeded! Received %d bytes of data",[receivedData length]);
// release the connection, and the data object
[connection release];
[receivedData release];
}
I am hoping this is a simple problem to solve, but I fear I am at the limit of my knowledge on this one and despite some serious Google searches and trying many different recommended approaches I am struggling to come up with a solution.
Eventually I will have a sophisticated caching mechanism for my app in which the table view checks the local machine for the images before going out and getting them form the server and maybe has a progress indicator until the images are retrieved. Right now even local image population can be sluggish if the image's are large enough using a synchronous process.
Any and all help would be very much appreciated.
Solution Update
In case anyone else needs a similar solution thanks to Ben's help here is what I came up with (generically modified for posting of course). Bear in mind that I have also implemented a custom caching of images and have made my image loading class generic enough to be used by various places in my app for calling images.
In my calling method, which in my case was a custom cell within a table...
ImageLoaderClass * myLoader = [[[ImageLoaderClass alloc] init] autorelease];
[myLoader fetchImageWithURL:#"/my/thumbnail/path/with/filename.png"
forMethod:#"myUniqueRef"
withId:1234
saveToCache:YES
cachePath:#"/path/to/my/custom/cache"];
This creates an instance of myLoader class and passes it 4 parameters. The URL of the image I want to get, a unique reference that I use to determine which class made the call when setting up the notification observers, the ID of the image, whether I want to save the image to cache or not and the path to the cache.
My ImageLoaderClass defines the method called above where I set what is passed from the calling cell:
-(void)fetchImageWithURL:(NSString *)imageURL
forMethod:(NSString *)methodPassed
withId:(int)imageIdPassed
saveToCache:(BOOL)shouldISaveThis
cachePath:(NSString *)cachePathToUse
{
NSURLRequest *theRequest=[NSURLRequest requestWithURL:[NSURL URLWithString:imageURL]
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:60.0];
// Create the connection with the request and start loading the data
NSURLConnection *theConnection=[[NSURLConnection alloc] initWithRequest:theRequest delegate:self];
if (theConnection) {
// Create the NSMutableData that will hold
// the received data
// receivedData is declared as a method instance elsewhere
receivedData = [[NSMutableData data] retain];
// Now set the variables from the calling class
[self setCallingMethod:methodPassed];
[self setImageId:imageIdPassed];
[self setSaveImage:shouldISaveThis];
[self setImageCachePath:cachePathToUse];
} else {
// Do something to tell the user the image could not be downloaded
}
}
In the connectionDidFinishLoading method I saved the file to cache if needed and made a notification call to any listening observers:
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
NSLog(#"Succeeded! Received %d bytes of data",[receivedData length]);
// Create an image representation to use if not saving to cache
// And create a dictionary to send with the notification
NSImage * mImage = [[NSImage alloc ] initWithData:receivedData];
NSMutableDictionary * mDict = [[NSMutableDictionary alloc] init];
// Add the ID into the dictionary so we can reference it if needed
[mDict setObject:[NSNumber numberWithInteger:imageId] forKey:#"imageId"];
if (saveImage)
{
// We just need to add the image to the dictionary and return it
// because we aren't saving it to the custom cache
// Put the mutable data into NSData so we can write it out
NSData * dataToSave = [[NSData alloc] initWithData:receivedData];
if (![dataToSave writeToFile:imageCachePath atomically:NO])
NSLog(#"An error occured writing out the file");
}
else
{
// Save the image to the custom cache
[mDict setObject:mImage forKey:#"image"];
}
// Now send the notification with the dictionary
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc postNotificationName:callingMethod object:self userInfo:mDict];
// And do some memory management cleanup
[mImage release];
[mDict release];
[connection release];
[receivedData release];
}
Finally in the table controller set up an observer to listen for the notification and send it off to the method to handle re-displaying the custom cell:
-(id)init
{
[super init];
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self selector:#selector(updateCellData:) name:#"myUniqueRef" object:nil];
return self;
}
Problem solved!
My solution is to use Grand Central Dispatch (GCD) for this purpose, you could save the image to disc too in the line after you got it from the server.
- (NSView *)tableView:(NSTableView *)_tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
{
SomeItem *item = [self.items objectAtIndex:row];
NSTableCellView *cell = [_tableView makeViewWithIdentifier:tableColumn.identifier owner:self];
if (item.artworkUrl)
{
cell.imageView.image = nil;
dispatch_async(dispatch_queue_create("getAsynchronIconsGDQueue", NULL),
^{
NSURL *url = [NSURL URLWithString:item.artworkUrl];
NSImage *image = [[NSImage alloc] initWithContentsOfURL:url];
cell.imageView.image = image;
});
}
else
{
cell.imageView.image = nil;
}
return cell;
}
(I am using Automatic Reference Counting (ARC) therefore there are no retain and release.)
Your intuition is correct; you want to have a callback from the object which is the NSURLConnection’s delegate to the controller which manages the table view, which would update your data source and then call -setNeedsDisplayInRect: with the rect of the row to which the image corresponds.
Have you tried using the initWithContentsOfURL: method?