What's the best approach to asynchronous image caching on the iPhone? - iphone

I'm creating an iPhone app that will pull data down from a Web API, including email addresses. I'd like to display an image associated with each email address in table cells, so I'm searching the Address Book for images and falling back on a default if the email address isn't in the book. This works great, but I have a few concerns:
Performance: The recipes I've found for looking for an address book record by Email address (or phone number) are reportedly rather slow. The reason for this is that one must iterate over every address book record, and for each one that has an image, iterate over all email addresses to find a match. This can be time-consuming for a large address book, of course.
Table Cells: So I thought I'd gather up all the email addresses for which I need to find images and find them all at once. This way I iterate through the book only once for all addresses. But this doesn't work well for table cells, where each cell corresponds to a single email address. I'd either have to gather all the images before displaying any cells (potentially slow), or have each cell look up each image as it loads (even slower, as I'd need to iterate through the book to find a match for each email address).
Asynchronous Lookup: So then I thought I'd look them up in bulk, but asynchronously, using NSInvocationOperation. For each image found in AddressBook, I'd save a thumbnail in the app sandbox. Then each cell could just reference this file and show the default if it doesn't exist (because it's not in the book or hasn't yet been found). If the image is later found in the asynchronous lookup, the next time the image needs to be displayed it would suddenly appear. This might work well for periodic regeneration of images (for when images have been changed in the address book, for example). But then for any given instance of my app, an image may not actually show up for a while.
Asynchronous Table Cell Lookup: Ideally, I'd use something like markjnet's asynchronous table cell updating to update table cells with an image once it has been downloaded. But for this to work, I'd have to spin off an NSInvocationOperation job for each cell as it's displayed and if the cached icon is missing from the sandbox. But then we're back to inefficiently iterating through the entire address book for each oneā€”and that can be a lot of them if you've just downloaded a whole bunch of new email addresses.
So my question is: How do others do this? I was fiddling with Tweetie2, and it looks like it updates displayed table cells asynchronously. I assume it's sending a separate HTTP request for every image it needs. If so, I imagine that searching the local address book by email address isn't any less efficient, so maybe that's the best approach? Just not worry about the performance issues associated with searching the address book?
If so, is saving a thumbnail image in the sandbox the best approach to caching? And if I wanted to create a new job to update all the thumbnails with any changes in the address book say once a day, what's the best approach to doing so?
How do the rest of you solve this sort of problem? Suggestions would be much appreciated!

Regardless of what strategy you use for the actual caching of images, I would only make one pass through the Address Book data each time you get a batch of email addresses, if possible. (And yes, I would do this asynchronously.)
Create an NSMutableDictionary which will serve as your in-memory cache for search results. Initialize this dictionary with each email address from the download as a key, with a sentinel as that key's value (such as [NSNull null]).
Next, iterate through each ABRecordRef in the Address Book, calling ABRecordCopyValue(record, kABPersonEmailProperty) and looping through the results in each ABMultiValue that is returned. If any of the email addresses are keys in your cache, set [NSNumber numberWithInt:ABRecordGetRecordId(record)] as the value of that key in your dictionary.
Using this dictionary as a lookup index, you can quickly obtain the images of ABRecordRefs for only the email addresses that you are currently displaying in your table view given the user's current scroll position, as suggested in hoopjones's answer. You can add an address book change listener to invalidate your cache, trigger another indexing operation, and then update the view, if your application needs that level of "up-to-date-ness".

I'd use the last method you listed (Asynchronous Table Cell Lookup) but only look images for the current records being displayed. I overload the UIScrollViewDelegate methods to find out when a user has stopped scrolling, and then only start making requests for the current visible cells.
Something like this (this is slightly modified from a tutorial I found on the web which I can't find now, apologies for not citing the author) :
- (void)loadContentForVisibleCells
{
NSArray *cells = [self.table visibleCells];
[cells retain];
for (int i = 0; i < [cells count]; i++)
{
// Go through each cell in the array and call its loadContent method if it responds to it.
AddressRecordTableCell *addressTableCell = (AddressRecordTableCell *)[[cells objectAtIndex: i] retain];
[addressTableCell loadImage];
[addressTableCell release];
addressTableCell = nil;
}
[cells release];
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;
{
// Method is called when the decelerating comes to a stop.
// Pass visible cells to the cell loading function. If possible change
// scrollView to a pointer to your table cell to avoid compiler warnings
[self loadContentForVisibleCells];
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate;
{
if (!decelerate)
{
[self loadContentForVisibleCells];
}
}
Once you know what address records are currently visible, just doing a search for those (5 -7 records probably) will be lightning fast. Once you grab the image, just cache it in a dictionary so that you don't have to redo the request for the image later.

You seem to try to implement lazy images loading in UITableView.
there's a good example from Apple, I'm referencing it here :
Lazy load images in UITableView

FYI, I've released a free, powerful, and easy library for doing asynchronous image loading and fast file caching: HJ Managed Objects
http://www.markj.net/asynchronous-loading-caching-images-iphone-hjobjman/

Related

JSQMessageViewController - Add a label inside chat bubble, on the side of the text

I'm using JSQMessageViewController for a simple chat app I'm building, and since I'm new in Swift and iOS development in general, I need some help on how to achieve this:
I want to add a timestamp inside the chat bubble. I read that I need to change some related xib files, but I'm not quite sure how to do that.
I found this, and marcuschoong's comment gets pretty close, but I'm not sure how to find these xibs' classes, and how to connect the IBOutlets to that class (I don't have any experience in Objective C either). I need the timestamp for both incoming and outgoing messages. Any help would be appreciated!
There are 2 xib files depending on the type of message, incoming or outgoing:
JSQMessageCollectionViewCellIncoming.xib
JSQMessageCollectionViewCellOutgoing.xib
Both have the same structure: avatar, bubble container, cellTopLabel, cellBottomLabel...
In your case, you can move either cellTopLabel or cellBottomLabel into the bubble container (and adjust their constraints). If you use cellBottomLabel, you'll need to modify the following method in order to show the timestamp for every message (by default the timestamp is only displayed every 3 messages, "indexPath.item % 3"):
- (NSAttributedString *)collectionView:(JSQMessagesCollectionView *)collectionView attributedTextForCellBottomLabelAtIndexPath:(NSIndexPath *)indexPath
{
JSQMessage *message = messages[indexPath.item];
return [[JSQMessagesTimestampFormatter sharedFormatter] attributedTimestampForDate:message.date];
}

Building a data driven UI

I have searched throughout SO on this topic for iPhone and everything points to the WWDC 2010 coverage, so yes I'm aware of that. But can anyone point me to a more detailed resource from where I can learn how to build a robust system for presenting different user interfaces on an app depending on the data that I'm presenting? I am fetching the data in a JSON format and my UI needs to vary depending on what I get out of the JSON parser.
Any books or online resources that give a detailed look into this topic?
Thanks!
I recently had the same issue in one of my apps (navigation style) and the way I solved it is fairly simple.
I had a user_type flag in my JSON response, and depending on that flag I would push a different view controller.
For example, given my JSON response is stored in a NSMutableDictionary called "response"
if ([response objectForKey:#"account_type"] == 1) {
/*
initialize user_type 1 viewController
*/
[self.navigationController pushViewController:userType1ViewController animated:YES];
else if ([response objectForKey:#"account_type"] == 2) {
/*
initialize user_type 2 viewController
*/
[self.navigationController pushViewController:userType2ViewController animated:YES];
}
You can have as many different user_types as you want.
Editing after clarification in comments
You will likely be manually drawing these views.
I would subclass UIView for each of the different question types you have listed (including properties for common elements like question title, choices, etc). Assuming that questions with the same question type will have the same layout.
Then you can cycle through your JSON response (once it's in an array or dictionary) and lay it out.
If it's a multiple choice question, make a new multipleChoiceQuestion view, add its properties, then addSubview to the main feed view. Then, for your next view, you will need to set the frame to be:
nextQuestion.frame = CGRectMake(0, 0 + firstQuestion.frame.size.height, height, width);
This will ensure that your second question is drawn right below the first question.

Proper Management Of A Singleton Data Store In IOS With Web Service

I'm currently using a singleton as a data store for my app. I essentially store a number of events that are pulled and parsed from a web service and then added as needed. Each time I make a request from the web service, I parse the results and see if the items already exist. If they do, I delete them and add the updated version provided by the web service.
Everything appeared to be working properly until I fired up the Instruments panel to find out that my system is leaking the objects every time it loads them from the web service (from the second time on). The core method where things appear to be messing up is this one, which is located in my HollerStore singleton class:
- (void)addHoller: (Holler *)h
{
//Take a holler, check to see if one like it already exists
int i = 0;
NSArray *theHollers = [NSArray arrayWithArray:allHollers];
for( Holler *th in theHollers )
{
if( [[th hollerId]isEqualToString:[h hollerId]] )
{
NSLog(#"Removing holler at index %i", i);
[allHollers removeObjectAtIndex:i];
}
i++;
}
[allHollers addObject:h];
}
Quick explanation: I decided to copy the allHollers NSMutableArray into theHollers because it's being updated asynchronously by NSURLConnection. If I update it directly, it results in a crash. As such, I switched to this model hoping to solve the problem, however the Instruments panel is telling me that my objects are leaking. All the counts are exactly the # of items I have in my data set.
From what I can tell removeObjectAtIndex isn't effectively removing the items. Would love to get the thoughts of anybody else out there on three things:
Is my analysis correct that something else must be retaining the individual hollers being added?
Should I be using CoreData or SQLite for storing information pulled from the web service?
Do you know how long data stored in a Singleton should be available for? Until the app is killed?
Update
I think I've found the source, however perhaps someone can provide some clarity on the proper way to do this. I've created a method called parseHoller which takes a dictionary object created through SBJSON and returns my own model (Holler). Here are the last couple lines:
Holler *h = [[[Holler alloc] initFromApiResponse:hollerId
creatorId:creatorId
creatorName:creatorName
creatorImageUrl:creatorImage
comments:comments
attendees:attendees
wishes:wishes
invitees:invites
createdAt:createdAt
text:text
title:title
when:when]autorelease];
//(some other autorelease stuff is here to clean up the internal method)
return h;
I figured that since I'm returning an autoreleased object, this should be fine. Do you see anything wrong with this?
Have you tried to do a retain count on the objects that is leaking? Maybe that could clear up when or where it is being retained.
The code should be
[putObjectHere retainCount];
and then write to an NSLog
Hope it gives you something
Peter

How to Load an array into OpenFlow

I'm trying to implement openFlow in my project but I cant seem to get the images to show up on my uiview. What isnt clear to me is once I have the dictionary of image links, how do i tell AFOpenView that I want to use that dictionary object as my data source?
I've looked at the demo code and I see that when the flickr request finishes, he saves a copy of the dictionary results, counts them, and then tells OpenFlowView that there are x number of images, but what is never clear is how he tells OpenFlowView to use the dictionary with the results?
- (void)flickrAPIRequest:(OFFlickrAPIRequest *)inRequest didCompleteWithResponse:(NSDictionary *)inResponseDictionary
{
// Hold onto the response dictionary.
interestingPhotosDictionary = [inResponseDictionary retain];
int numberOfImages = [[inResponseDictionary valueForKeyPath:#"photos.photo"] count];
[(AFOpenFlowView *)self.view setNumberOfImages:numberOfImages];
}
See here: http://blog.objectgraph.com/index.php/2010/04/09/how-to-add-coverflow-effect-on-your-iphone-app-openflow/
This tutorial seems to suggest that you have to call the view's setImage method multiple times, once per image.
This tells me that the implementation is confusing and weird, but for this you have to blame the component's author.
The images are loaded on demand in the 'updateCoverImage:' method of AFOpenFlowView.m
'updateCoverImage:' calls 'openFlowView:requestImageForIndex:' in AFOpenFlowViewController.m, which uses interestingPhotosDictionary.
So, it is called on demand whenever an image needs to be loaded. It wraps an operation queue so the images are loaded outside the main thread.

how to make asynchronous call using NSThread

i have made one list of images + respective data in tableview.
it takes long time while loading
i want to make multithreading two methods
1> parsing of data
2> parsing of images
i want to execute parsing of data first after that i can select any of rows listed even though images not been loaded(/parsed) because the images is parsed after the parsing of data and it takes long time.
from where should i call these both the methods.
and how enable the selection on row after parsing of the data...
how to do multithread both the methods
waiting for your great responce
Thanking in advance
You likely don't want to use NSThreads - at least not directly.
What you do is subclass NSOperation.
There are a few ways to do what you have in mind. If you know the total number of rows in your table right from the start, then things are simpler:
Make a subclass of NSOperation called MyParseDataOperation. Then make one MyParseDataOperation for each row in your table. When the operation is done, you need to message your main thread with the resulting data.
Code below is full of errors, incomplete. etc.
ie in your MyParseDataOperation class:
MyParseDataOperation
-(id)initWithStuff:(NSURL*)stuff forTableRow:(int)row;
{
blah blah -
// here is where I make sure I have all the data I need for main() which is called in the background on some random thread at some future time.
}
-(void)main;
{
// use data like Urls, file names, etc passed in to the initWithStuff method
get stuff
parse stuff
// ok now you have the data
NSMutableDictionary* parsedData = [NSMutableDictionary dictionary];
[parsedData setObject:[NSNumber numberWithInt:row] forKey:#"row"];
[parsedData setObject:stuff i figured out forKey:#parsed];
[tableDataSource performSelectorOnMainThread:#selector(dataParsed) withObject:parsedData];
}