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).
Related
I have something along the line of this :
#implementation ImageLoader
NSMutableDictionary *_tasks;
- (void) loadImageWithURL:(NSURL *)url callback:(SMLoaderCallback)callback {
NSMutableArray *taskList = [_tasks objectForKey:urlString];
if (taskList == nil) {
taskList = [[NSMutableArray alloc] initWithCapacity:5];
[taskList addObject:callback];
[_tasks setObject:taskList forKey:urlString];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
dispatch_async(dispatch_get_main_queue(), ^{
UIImage *image = [[UIImage alloc] initWithData:data];
for (SMLoaderCallback cb in taskList) {
cb(image, nil);
}
[_tasks removeObjectForKey:url.absoluteString];
});
});
} else {
[taskList addObject:callback];
}
}
#end
In here, I'm trying to queue up image downloads to gain performance (as an exercise only). So I'm keeping a NSDictionary that maps an URL with an array of callbacks to be called whenever the data is downloaded.
Once the image is downloaded, I no longer need this array, so I remove it from the dictionary.
I would like to know if, in this code (with ARC enabled), my array along with the callbacks are correctly released when I call [_tasks removeObjectForKey:url.absoluteString].
If your project uses ARC and that dictionary is the only thing that is referencing to those values, yes. it will be remove permanently.
ARC keeps track of number of objects that is pointing to some other object and it will remove it as soon as the count reaches 0.
so adding to dictionary -> reference count += 1
removing from dictionary -> reference count -= 1
Somewhe
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 :)
I'm writing an application that will take several images from URL's, turn them into a UIImage and then add them to the photo library and then to the custom album. I don't believe its possible to add them to a custom album without having them in the Camera Roll, so I'm accepting it as impossible (but it would be ideal if this is possible).
My problem is that I'm using the code from this site and it does work, but once it's dealing with larger photos it returns a few as 'Write Busy'. I have successfully got them all to save if I copy the function inside its own completion code and then again inside the next one and so on until 6 (the most I saw it take was 3-4 but I don't know the size of the images and I could get some really big ones) - this has lead to the problem that they weren't all included in the custom album as they error'd at this stage too and there was no block in place to get it to repeat.
I understand that the actual image saving is moved to a background thread (although I don't specifically set this) as my code returns as all done before errors start appearing, but ideally I need to queue up images to be saved on a single background thread so they happen synchronously but do not freeze the UI.
My code looks like this:
UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:singleImage]]];
[self.library saveImage:image toAlbum:#"Test Album" withCompletionBlock:^(NSError *error) {
if (error!=nil) {
NSLog(#"Error");
}
}];
I've removed the repetition of the code otherwise the code sample would be very long! It was previously where the NSLog code existed.
For my test sample I am dealing with 25 images, but this could easily be 200 or so, and could be very high resolution, so I need something that's able to reliably do this over and over again without missing several images.
thanks
Rob
I've managed to make it work by stripping out the save image code and moving it into its own function which calls itself recursively on an array on objects, if it fails it re-parses the same image back into the function until it works successfully and will display 'Done' when complete. Because I'm using the completedBlock: from the function to complete the loop, its only running one file save per run.
This is the code I used recursively:
- (void)saveImage {
if(self.thisImage)
{
[self.library saveImage:self.thisImage toAlbum:#"Test Album" withCompletionBlock:^(NSError *error) {
if (error!=nil) {
[self saveImage];
}
else
{
[self.imageData removeObject:self.singleImageData];
NSLog(#"Success!");
self.singleImageData = [self.imageData lastObject];
self.thisImage = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:self.singleImageData]]];
[self saveImage];
}
}];
}
else
{
self.singleImageData = nil;
self.thisImage = nil;
self.imageData = nil;
self.images = nil;
NSLog(#"Done!");
}
}
To set this up, I originally used an array of UIImages's but this used a lot of memory and was very slow (I was testing up to 400 photos). I found a much better way to do it was to store an NSMutableArray of URL's as NSString's and then perform the NSData GET within the function.
The following code is what sets up the NSMutableArray with data and then calls the function. It also sets the first UIImage into memory and stores it under self.thisImage:
NSEnumerator *e = [allDataArray objectEnumerator];
NSDictionary *object;
while (object = [e nextObject]) {
NSArray *imagesArray = [object objectForKey:#"images"];
NSString *singleImage = [[imagesArray objectAtIndex:0] objectForKey:#"source"];
[self.imageData addObject:singleImage];
}
self.singleImageData = [self.imageData lastObject];
self.thisImage = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:self.singleImageData]]];
[self saveImage];
This means the rest of the getters for UIImage can be contained in the function and the single instance of UIImage can be monitored. I also log the raw URL into self.singleImageData so that I can remove the correct elements from the array to stop duplication.
These are the variables I used:
self.images = [[NSMutableArray alloc] init];
self.thisImage = [[UIImage alloc] init];
self.imageData = [[NSMutableArray alloc] init];
self.singleImageData = [[NSString alloc] init];
This answer should work for anyone using http://www.touch-code-magazine.com/ios5-saving-photos-in-custom-photo-album-category-for-download/ for iOS 6 (tested on iOS 6.1) and should result in all pictures being saved correctly and without errors.
If saveImage:toAlbum:withCompletionBlock it's using dispatch_async i fear that for i/o operations too many threads are spawned: each write task you trigger is blocked by the previous one (bacause is still doing I/O on the same queue), so gcd will create a new thread (usually dispatch_async on the global_queue is optimized by gcd by using an optimized number of threads).
You should either use semaphores to limit the write operation to a fixed number at the same time or use dispatch_io_ functions that are available from iOS 5 if i'm not mistaken.
There are plenty example on how to do this with both methods.
some on the fly code for giving an idea:
dispatch_semaphore_t aSemaphore = dispatch_semaphore_create(4);
dispatch_queue_t ioQueue = dispatch_queue_create("com.customqueue", NULL);
// dispatch the following block to the ioQueue
// ( for loop with all images )
dispatch_semaphore_wait(aSemaphore , DISPATCH_TIME_FOREVER);
[self.library saveImage:image
toAlbum:#"Test Album"
withCompletionBlock:^(NSError *error){
dispatch_semaphore_signal(aSemaphore);
}];
so every time you will have maximum 4 saveImage:toAlbum, as soon as one completes another one will start.
you have to create a custom queue, like above (the ioQueue) where to dispatch the code that does the for loop on the images, so when the semaphore is waiting the main thread is not blocked.
I want to get a photo from my homepage and display it. And it (kind of) works. But sometimes it takes min 10 seconds to load the next scene because of something that happens here. So here is what I do :
NSString *myURL = [PICURL stringByAppendingString:[[[[levelConfig objectForKey:category] objectForKey:lSet] objectForKey:levelString] objectForKey:#"pic"]];
UIImage *dYKPic = [UIImage imageWithData: [NSData dataWithContentsOfURL: [NSURL URLWithString:myURL]]];
if(dYKPic == nil){
NSString *defaultURL = #"http://www.exampleHP.com/exampleFolder/default.jpg";
dYKPic = [UIImage imageWithData: [NSData dataWithContentsOfURL: [NSURL URLWithString:defaultURL]]];
}
CCTexture2D *tex = [[CCTexture2D alloc] initWithImage:dYKPic];
CCSprite *image = [CCSprite spriteWithTexture:tex];
image.anchorPoint = ccp(0,0);
image.position = ccp(32,216);
[self addChild:image z:2];
So, it takes 10 seconds, and additionally, the default.jpg is loaded - even though the picture exists - but that just in the case where it takes so long... 70% of the cases it works perfectly normal... So what is wrong ? Where do I release tex ? Immediately after adding the child ?
It has to load the picture. Thats the issue. You either need to load and cache it, store it, or preload it before you need it.
Or one final option is the load it async and just update your view when its finished.
I have an NSTableView that lists tags that are stored using Core Data. The default value for a tag is 'untitled' and I need each tag to be unique, so I have a validation routine that traps empty and non-unique values and that works fine. I don't want the user to be able to store the 'untitled' value for a tag, so I am observing the NSControlTextDidEndEditingNotification, which calls the following code:
- (void)textEndedEditing:(NSNotification *)note {
NSString *enteredName = [[[note userInfo] valueForKey:#"NSFieldEditor"] string];
if ([enteredName isEqualToString:defaultTagName]) {
NSString *dString = [NSString stringWithFormat:#"Rejected - Name cannot be default value of '%#'", defaultTagName];
NSString *errDescription = NSLocalizedStringFromTable( dString, #"Tag", #"validation: default name error");
NSString *errRecoverySuggestion = NSLocalizedStringFromTable(#"Make sure you enter a unique value for the new tag.", #"Tag", #"validation: default name error suggestion");
int errCode = TAG_NAME_DEFAULT_VALUE_ERROR_CODE;
NSArray *objArray = [NSArray arrayWithObjects:errDescription, errRecoverySuggestion, nil];
NSArray *keyArray = [NSArray arrayWithObjects:NSLocalizedDescriptionKey, NSLocalizedRecoverySuggestionErrorKey, nil];
NSDictionary *eDict = [NSDictionary dictionaryWithObjects:objArray forKeys:keyArray];
NSError *error = [[NSError alloc] initWithDomain:TAG_ERROR_DOMAIN code:errCode userInfo:eDict];
NSBeep();
[preferencesWindowsController presentError:error];
unsigned long index = [self rowWithDefaultTag];
[self selectRowIndexes:[NSIndexSet indexSetWithIndex:index] byExtendingSelection:NO];
// [self editColumn:0 row:index withEvent:nil select:YES];
}
}
- (unsigned long)rowWithDefaultTag {
__block unsigned long returnInt;
[managedTags enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
if ([[obj valueForKey:#"name"] isEqualToString:defaultTagName]) {
returnInt = idx;
*stop = YES;
}
}];
return returnInt;
}
With the 'editColumn' line commented out, the code works, so if the user accepts the default tag name without editing it, the error is built, displayed and the process finishes by leaving the appropriate row in the table highlighted.
However, I would like to take it that step further and place the user in edit mode. When I uncomment the 'editColumn' line, the behaviour is not at all what I expected - the tableView loses its blue focus box and the row that respresents the new tag is blank. If I click on the tableView, the row becomes visible. I've spent a lot of time on this and have got nowhere, so some help with this would be very much appreciated.
(Note: I tried using textDidEndEditing, which also didn't behave as I expected, but that is a separate issue!)
Answering my own question. Doh!
I already had a method which I used to put the user in edit mode when they clicked the button to add a new tag:
- (void)objectAdded:(NSNotification *)note {
if ([[note object] isEqual:self]) {
[self editColumn:0 row:[self rowWithDefaultTag] withEvent:nil select:YES];
}
}
Creating a notification to call this solves the problem and places the user in edit mode correctly. The important thing is not to try to do this on the existing runloop; so sending the notification as follows postpones delivery until a later runloop:
// OBJECTADDED is a previously defined constant.
NSNotification * note = [NSNotification notificationWithName:OBJECTADDED object:self];
[[NSNotificationQueue defaultQueue] enqueueNotification: note postingStyle: NSPostWhenIdle];
Problem solved. I wasted a lot of time trying to solve this - a classic example of getting too involved in the code and not looking at what I'm trying to do.
I've forgotten where I first saw this posted - whoever you are, thank you!