I am using asynchronous loading of images in a cell.
My code is:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *const ImageCellId = #"ImageCell";
PDATableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ImageCellId];
if (cell == nil) {
cell = [[PDATableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:ImageCellId];
}
Tutorial *thisTutorial = [_objects objectAtIndex:indexPath.row];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSURL *tutorialsUrl8 = [NSURL URLWithString:thisTutorial.url]];
NSData *tutorialsHtmlData2 = [NSData dataWithContentsOfURL:tutorialsUrl8];
TFHpple *tutorialsParser2 = [TFHpple hppleWithHTMLData:tutorialsHtmlData2];
NSString *tutorialsXpathQueryString2 = #"//div[#class='photo']/img";
NSArray *tutorialsNodes2 = [tutorialsParser2 searchWithXPathQuery:tutorialsXpathQueryString2];
for (TFHppleElement *element2 in tutorialsNodes2) {
tutorial2 = [[Tutorial alloc] init];
tutorial2.url = [element2 objectForKey:#"src"];
data2 = [[NSData alloc] initWithContentsOfURL:[NSURL URLWithString:tutorial2.url]];
}
if(data2)
{
imageMain = [UIImage imageWithData:data2];
if (imageMain) {
dispatch_async(dispatch_get_main_queue(), ^{
if (cell)
cell.cellImageView.image = imageMain;
});
}
}
});
return cell;
}
It works! But! When I scroll my TebleView, I have images loaded again.
What am I doing wrong?
You are loading the images asynchronously. Thats fine but you are not applying any caching mechanism.
By caching mechanism i mean a way which checks whether the image is already downloaded or not. If downloaded once then it should not download the image again instead use the already downloaded image.
These downloaded images should be saved locally so that they can be used every time the app is active. Its easy to implement this image caching mechanism but its better to use already existing api's which are much improved and optimized.
SDWebImage is the most optimized way of loading images asynchronously.
You can use SDwebImage, downloads the images and keeps them in cache memory (to speed up) and saves images locally.
Also in order to know other caching apis you can check this. I will always prefer SDWebImage as its really good and fast. link.
Hope this will help you. Happy coding :)
Related
according to the apple documentation it is recommended to use xcassets for iOS7 applications and reference those images over imageNamed.
But as far as I'm aware, there were always problems with imageNamed and memory.
So I made a short test application - referencing images out of the xcassets catalogue with imageNamed and started the profiler ... the result was as expected. Once allocated memory wasn't released again, even after I removed the ImageView from superview and set it to nil.
I'm currently working on an iPad application with many large images and this strange imageView behavior leads to memory warnings.
But in my tests I wasn't able to access xcassets images over imageWithContentsOfFile.
So what is the best approach to work with large images on iOS7? Is there a way to access images from the xcassets catalogue in another (more performant) way? Or shouldn't I use xcassets at all so that I can work with imageWithContentsOfFile?
Thank you for your answers!
UPDATE: Cache eviction works fines (at least since iOS 8.3).
I decided to go with the "new Images.xcassets" from Apple, too. Things started to go bad, when I had about 350mb of images in the App and the App constantly crashed (on a Retina iPad; probably because of the size of the loaded images).
I have written a very simple test app where I load the images in three different types (watching the profiler):
imageNamed: loaded from an asset: images never gets released and the app crashes (for me I could load 400 images, but it really depends on the image size)
imageNamed: (conventionally included to the project): The memory usage is high and once in a while (> 400 images) I see a call to didReceiveMemoryWarning:, but the app is running fine.
imageWithContentsOfFile([[NSBundle mainBundle] pathForResource:...): The memory usage is very low (<20mb) because the images are only loaded once at a time.
I really would not blame the caching of the imageNamed: method for everything as caching is a good idea if you have to show your images again and again, but it is kind of sad that Apple did not implement it for the assets (or did not document it that it is not implemented). In my use-case, I will go for the non-caching imageWithData because the user won't see the images again.
As my app is almost final and I really like the usage of the loading mechanism to find the right image automatically, I decided to wrap the usage:
I removed the images.xcasset from the project-target-copy-phase and added all images "again" to the project and the copy-phase (simply add the top level folder of Images.xcassets directly and make sure that the checkbox "Add To Target xxx" is checked and "Create groups for any added folders" (I did not bother about the useless Contents.json files).
During first build check for new warnings if multiple images have the same name (and rename them in a consistent way).
For App Icon and Launch Images set "Don't use asset catalog" in project-target-general and reference them manually there.
I have written a shell script to generate a json-model from all the Contents.json files (to have the information as Apples uses it in its asset access code)
Script:
cd projectFolderWithImageAsset
echo "{\"assets\": [" > a.json
find Images.xcassets/ -name \*.json | while read jsonfile; do
tmppath=${jsonfile%.imageset/*}
assetname=${tmppath##*/}
echo "{\"assetname\":\"${assetname}\",\"content\":" >> a.json
cat $jsonfile >> a.json;
echo '},' >>a.json
done
echo ']}' >>a.json
Remove the last "," comma from json output as I did not bother to do it manually here.
I have used the following app to generate json-model-access code: https://itunes.apple.com/de/app/json-accelerator/id511324989?mt=12 (currently free) with prefix IMGA
I have written a nice category using method swizzling in order to not change running code (and hopefully removing my code very soon):
(implementation not complete for all devices and fallback mechanisms!!)
#import "UIImage+Extension.h"
#import <objc/objc-runtime.h>
#import "IMGADataModels.h"
#implementation UIImage (UIImage_Extension)
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
Method imageNamed = class_getClassMethod(class, #selector(imageNamed:));
Method imageNamedCustom = class_getClassMethod(class, #selector(imageNamedCustom:));
method_exchangeImplementations(imageNamed, imageNamedCustom);
});
}
+ (IMGABaseClass*)model {
static NSString * const jsonFile = #"a";
static IMGABaseClass *baseClass = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSString *fileFilePath = [[NSBundle mainBundle] pathForResource:jsonFile ofType:#"json"];
NSData* myData = [NSData dataWithContentsOfFile:fileFilePath];
__autoreleasing NSError* error = nil;
id result = [NSJSONSerialization JSONObjectWithData:myData
options:kNilOptions error:&error];
if (error != nil) {
ErrorLog(#"Could not load file %#. The App will be totally broken!!!", jsonFile);
} else {
baseClass = [[IMGABaseClass alloc] initWithDictionary:result];
}
});
return baseClass;
}
+ (UIImage *)imageNamedCustom:(NSString *)name{
NSString *imageFileName = nil;
IMGAContent *imgContent = nil;
CGFloat scale = 2;
for (IMGAAssets *asset in [[self model] assets]) {
if ([name isEqualToString: [asset assetname]]) {
imgContent = [asset content];
break;
}
}
if (!imgContent) {
ErrorLog(#"No image named %# found", name);
}
if (is4InchScreen) {
for (IMGAImages *image in [imgContent images]) {
if ([#"retina4" isEqualToString:[image subtype]]) {
imageFileName = [image filename];
break;
}
}
} else {
if ( UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone ) {
for (IMGAImages *image in [imgContent images]) {
if ([#"iphone" isEqualToString:[image idiom]] && ![#"retina4" isEqualToString:[image subtype]]) {
imageFileName = [image filename];
break;
}
}
} else {
if (isRetinaScreen) {
for (IMGAImages *image in [imgContent images]) {
if ([#"universal" isEqualToString:[image idiom]] && [#"2x" isEqualToString:[image scale]]) {
imageFileName = [image filename];
break;
}
}
} else {
for (IMGAImages *image in [imgContent images]) {
if ([#"universal" isEqualToString:[image idiom]] && [#"1x" isEqualToString:[image scale]]) {
imageFileName = [image filename];
if (nil == imageFileName) {
// fallback to 2x version for iPad unretina
for (IMGAImages *image in [imgContent images]) {
if ([#"universal" isEqualToString:[image idiom]] && [#"2x" isEqualToString:[image scale]]) {
imageFileName = [image filename];
break;
}
}
} else {
scale = 1;
break;
}
}
}
}
}
}
if (!imageFileName) {
ErrorLog(#"No image file name found for named image %#", name);
}
NSString *imageName = [[NSBundle mainBundle] pathForResource:imageFileName ofType:#""];
NSData *imgData = [NSData dataWithContentsOfFile:imageName];
if (!imgData) {
ErrorLog(#"No image file found for named image %#", name);
}
UIImage *image = [UIImage imageWithData:imgData scale:scale];
DebugVerboseLog(#"%#", imageFileName);
return image;
}
#end
I am parsing some images and strings from a JSON file, the parsing works fine, but the image loading is very slow. I notized, the UITableView shows the content quicker, when I press on the UITableViewCell. Does anyone know a fix for that?
Here is the code I use, I use a NSOperationQueue to keep the CPU usage low.
NSDictionary *dict;
dict = [application objectAtIndex:indexPath.row];
name = [dict objectForKey:#"name"];
detileName = [dict objectForKey:#"detailName"];
itmsLink = [dict objectForKey:#"itms-serviceLink"];
icon = [dict objectForKey:#"icon"];
developer = [dict objectForKey:#"developer"];
version = [dict objectForKey:#"version"];
category = [dict objectForKey:#"category"];
rating = [dict objectForKey:#"rating"];
ratingNumbers = [dict objectForKey:#"ratingNumber"];
description = [dict objectForKey:#"description"];
developerEmails = [dict objectForKey:#"developerEmail"];
[downloadQueue addOperationWithBlock:^{
cell.AppName.text = name;
cell.category.text = category;
cell.rater.text = [NSString stringWithFormat:#"(%#)", ratingNumbers];
if ([rating intValue] == 1) {
cell.rating.image = [UIImage imageNamed:#"1.png"];
}
if ([rating intValue] == 2) {
cell.rating.image = [UIImage imageNamed:#"2.png"];
}
if ([rating intValue] == 3) {
cell.rating.image = [UIImage imageNamed:#"3.png"];
}
if ([rating intValue] == 4) {
cell.rating.image = [UIImage imageNamed:#"4.png"];
}
cell.itms = itmsLink;
cell.AppIcon.image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:icon]]];
cell.number.text = [NSString stringWithFormat:#"%li", (long)indexPath.row + 1];
}];
It looks like you've already got most of the data you need to present the cell, except for image that will go in AppIcon.image. the way things are set up now, the image download is blocking you from presenting the cell immediately. my guess is that the image download does eventually complete, but you do not force the cell to redraw itself after the download has finished. tapping on a cell forces it to redraw itself, which is probably why you're seeing the behavior you described.
I suggest you present the cell immediately using the data that you already have downloaded, and kick off a background download of the image. When the download is complete, you can send an NSNotification and update the appropriate cell. you can do this by creating a subclass of NSOperation that accepts a URL during initialization, and then adding that op to your operation queue
if you don't want to do all that work yourself, there is a category on UIImageView that uses AFNetworking to do the update for you using blocks.
https://github.com/zbowling/AFNetworkingPollTest/blob/master/ServerTest/UIImageView%2BAFNetworking.h
As stated by #Nick Galasso - you don't refresh the cell's and passing the cell pointer to a block is a very bad practice since UITableView actually reuses the cell's and when download is complete you should obtain cell pointer again - you are interested in cell under actual NSIndexPath, not the object.
Under a link:
https://developer.apple.com/library/ios/samplecode/LazyTableImages/Introduction/Intro.html
You can find perfect sample code on lazy image loading, fragment below shows the part which actually brings the view, this code called on download finish set's the image asap:
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
cell.imageView.image = downloadedImage;
You can call it in some completion handler of downloading class (the sample under Apple link covers that).
I am currently building an application that generates images and stores them in an NSMutableArray, which then used in a UINavigation (Cell.imageView.image). I need to be able to handle up to 2000 images without causing lag in my application.
Currently how I set this generating is by calling the generating method when cellForRowAtIndexPath is accessed. Which seems to cause a 4-5 second lag before the next navigation is called.
fortunately after those 4-5 seconds the generating is done and there is no issues.
In the world of iProducts waiting 4-5 seconds isn't really an option. I am wondering what my options are for generating these images in the background. I tried using threads [self performSelectorInBackground:#selector(presetSnapshots) withObject:nil];
but that only gave me issues about vectors for some reason.
Heres the generating code:
-(void)presetSnapshots{
//NSAutoreleasePool* autoReleasePool = [[NSAutoreleasePool alloc] init];
for (int i = 0; i < [selectedPresets count]; ++i){
GraphInfo* graphInfo = [selectedPresets objectAtIndex:i];
graphInfo.snapshot = [avc takePictureOfGraphInfo:graphInfo PreserveCurrentGraph:false];
[graphInfo.snapshot retain];
}
//[autoReleasePool drain];
presetSnapshotFinished = YES;
}
inside - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { is
if (presetSnapshotFinished == NO){
[self presetSnapshots];
//[self performSelectorInBackground:#selector(presetSnapshots) withObject:nil];
}
cell.imageView.image = [[selectedPresets objectAtIndex:indexPath.row] snapshot];
Edit:
I also rather not use coreData for this. The images are 23x23 and coming out to about 7kb. So its about 6MB being use at any giving time in memory.
You can use Grand Central Dispatch (GCD) to fire up [self presetSnapshots]
dispatch_queue_t working_queue = dispatch_queue_create("com.yourcompany.image_processing", NULL);
dispatch_queue_t high = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH,NULL);
dispatch_set_target_queue(working_queue,high);
dispatch_queue_t main_queue = dispatch_get_main_queue();
dispatch_async(working_queue,^{
if (presetSnapshotFinished == NO){
[self presetSnapshots];
}
dispatch_async(main_queue,^{
cell.imageView.image = [[selectedPresets objectAtIndex:indexPath.row] snapshot];
});
});
I am trying to asynchronously download images for a UITableViewCell, but it is currently setting the same image to each cell.
Please can you tell me the problem with my code:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease];
}
SearchObject *so = (SearchObject *)[_tableData objectAtIndex:indexPath.row];
cell.textLabel.text = [[[[so tweet] stringByReplacingOccurrencesOfString:#""" withString:#"\""] stringByReplacingOccurrencesOfString:#"<" withString:#"<"] stringByReplacingOccurrencesOfString:#">" withString:#">"];
cell.detailTextLabel.text = [so fromUser];
if (cell.imageView.image == nil) {
NSURLRequest *req = [NSURLRequest requestWithURL:[NSURL URLWithString:[so userProfileImageURL]]];
NSURLConnection *conn = [NSURLConnection connectionWithRequest:req delegate:self];
[conn start];
}
if ([_cellImages count] > indexPath.row) {
cell.imageView.image = [UIImage imageWithData:[_cellImages objectAtIndex:indexPath.row]];
}
return cell;
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[_cellData appendData:data];
[_cellImages addObject:_cellData];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
[self.tableView reloadData];
}
You are appending the data from every image downloaded to the same data object. So in the best case the data object ends up with the data for image #1 immediately followed by the data for image #2 and so on; the image decoder is apparently taking the first image in the chunk of data and ignoring the garbage after. You also seem to be unaware that NSURLConnections' connection:didReceiveData: will not necessarily be called in the order that the connections were started, that connection:didReceiveData: can be called zero or multiple times per connection (and probably will if your images are more than a few kibibytes), and that tableView:cellForRowAtIndexPath: is not guaranteed to be called for every cell in the table in order. All of which are going to totally screw up your _cellImages array.
To do this right, you need to have a separate NSMutableData instance for each connection, and you need to add it to your _cellImages array just once, and at the correct index for the row rather than at the arbitrary next available index. And then in connection:didReceiveData: you need to figure out the correct NSMutableData instance to append to; this could be done by using the connection object (wrapped in an NSValue using valueWithNonretainedObject:) as the key in an NSMutableDictionary, or using objc_setAssociatedObject to attach the data object to the connection object, or by making yourself a class that handles all the management of the NSURLConnection for you and hands you the data object when complete.
I don't know if this is causing the problem or not, but in your connection:didReceiveData: method you're just appending the image data to the array; you should be storing the image data in such a way that you can link it to the cell it's supposed to be shown in. One way to do this would be use an NSMutableArray populated with a bunch of [NSNull]s, then replace the null value at the appropriate index when the connection has finished loading.
Also, you're appending the _cellData to the _cellImages array when the connection hasn't finished loading, you should only be doing this in the connection:didFinishLoading method.
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?