Multiple NSURLSessions Causing UITableView Problems - ios

I'm running into a bit of a strange problem here. One of my NSURLSessions is in charge of getting information for restaurant information that I have stored (restaurant name, restaurant's logo URL, etc), and then the second NSURLSession is in charge of using the restaurant's logo URL to retrieve the specific image and set it for each UITableView's cell.
The problem, however, is that my UITableView does not load anything at all sometimes so the cells are empty, but at other times when I add an extra [_tableView reload] in the NSURLSessions' completion block in the fetchPosts method, it'll work, but then the cells will stop displaying anything again if I re-run it. Something is definitely wrong. Have a look at my code below:
#import "MainViewController.h"
#import "SWRevealViewController.h"
#import "RestaurantNameViewCell.h"
#import "RestaurantList.h"
#interface MainViewController ()
#end
#implementation MainViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//List of restaurants needed to load home page
_restaurantInformationArray = [[NSMutableArray alloc] init];
self.tableView.dataSource = self;
self.tableView.delegate = self;
//setup for sidebar
SWRevealViewController *revealViewController = self.revealViewController;
if ( revealViewController )
{
[self.sidebarButton setTarget: self.revealViewController];
[self.sidebarButton setAction: #selector( revealToggle: )];
[self.view addGestureRecognizer:self.revealViewController.panGestureRecognizer];
}
//Get list of restaurants and their image URLs
[self fetchPosts];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [_restaurantInformationArray count];
}
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
RestaurantNameViewCell *cell = (RestaurantNameViewCell *)[_tableView dequeueReusableCellWithIdentifier:#"restaurantName" forIndexPath:indexPath];
RestaurantList *currentRestaurant = [_restaurantInformationArray objectAtIndex:indexPath.row];
cell.restaurantName.text = currentRestaurant.name;
cell.imageAddress = currentRestaurant.imageURL;
cell.restaurantClicked = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tapDetected:)];
cell.restaurantClicked.numberOfTapsRequired = 1;
cell.restaurantLogo.userInteractionEnabled = YES;
[cell.restaurantLogo addGestureRecognizer:cell.restaurantClicked];
cell.restaurantLogo.tag = indexPath.row;
//Add restaurant logo image:
NSString *URL = [NSString stringWithFormat:#"http://private.com/images/%#.png",cell.imageAddress];
NSURL *url = [NSURL URLWithString:URL];
NSURLSessionDownloadTask *downloadLogo = [[NSURLSession sharedSession]downloadTaskWithURL:url completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
UIImage *downloadedImage = [UIImage imageWithData:[NSData dataWithContentsOfURL:location]];
cell.restaurantLogo.image = downloadedImage;
}];
[downloadLogo resume];
return cell;
}
-(void)fetchPosts {
NSString *address = #"http://localhost/xampp/restaurants.php";
NSURL *url = [NSURL URLWithString:address];
NSURLSessionDataTask *downloadRestaurants = [[NSURLSession sharedSession]dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSError *someError;
NSArray *restaurantInfo = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&someError];
for(NSDictionary *dict in restaurantInfo) {
RestaurantList *newRestaurant = [[RestaurantList alloc]init];
newRestaurant.name = [dict valueForKey:#"name"];
newRestaurant.imageURL = [dict valueForKey:#"image"];
[_restaurantInformationArray addObject:newRestaurant];
//Refresh table view to make sure the cells have info AFTER the above stuff is done
[_tableView reloadData];
}
}];
[downloadRestaurants resume];
}
#end
It's probably a very stupid mistake that I'm making, but I'm not certain how I should correct this. I'm new to iOS development, so would greatly appreciate some guidance :)

Besides assuming that your network requests aren't erroring (you should at least log if there are network errors), there are threading issues.
Your NSURLSession callback probably runs on a background thread. This makes it unsafe to call UIKit (aka - [_tableView reloadData]). UIKit isn't thread safe. This means invoking any of UIKit's APIs from another thread creates non-deterministic behavior. You'll want to run that piece of code on the main thread:
dispatch_async(dispatch_get_main_queue(), ^{
[_tableView reloadData];
});
Likewise for fetching the images. It's slightly more complicated because of table view cell reuse which could cause the wrong image to display when scrolling. This is because the same cell instance is used for multiple values in your array as the user scrolls. When any of those callbacks trigger, it'll replace whatever image happens to be in that cell. The generally steps to reproduce this is as follows:
TableView requests 5 cells
MainViewController requests 5 images (one for each cell)
User scrolls down one cell
The first cell gets reused as the 6th cell.
MainViewController requests another image for the 6th cell.
The 6th image is retrieved, the callback is triggered, image of the first cell is set to image #6.
The 1st image is retrieved, the callback is triggered, image of the first cell is set to image #1 (incorrect).
You'll need to make sure the cell is displaying the correct cell before attempting to assign the image to it. If you rather not implement that logic for image fetching in cells, you could use SDWebImage instead. Using SDWebImage's [UIImageView sd_setImageWithURL:] is thread safe (it will set the image on the main thread).
Side notes:
You only need to reload data once all your changes are in _restaurantInformationArray, and not every time in the for loop.

Related

Passing a UIImage from a viewcontroller to another

I want to pass a UIImage from a View Controller to another one, but it doesn't work. I actually get (null) if I log the UIImage Value in the second View Controller.
The Image File comes from parse.com. And it works absolutely fine in the first view controller. But as soon as I pass it to the second view controller, the image won't work there.
Here's the code:
My prepareForSegue in the .m file of the firstViewController (BookListTableViewController)
- (void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([segue.identifier isEqualToString:#"ShowBookDetailSegue"]) {
NSIndexPath *indexPath = [self.bookListTableView indexPathForSelectedRow];
ParseBookDetailTableViewController *pbdtvc = segue.destinationViewController;
PFObject *tempObject = [totalStrings objectAtIndex:indexPath.row];
PFFile *eventImage = [tempObject objectForKey:#"bookImage"];
if(eventImage != NULL)
{
[eventImage getDataInBackgroundWithBlock:^(NSData *imageData, NSError *error)
{
UIImage *thumbnailImage = [UIImage imageWithData:imageData];
pbdtvc.bookImageDetail = thumbnailImage;
}];
}
my .h file of the secondViewController (ParseBookDetailViewController)
#property (strong, nonatomic) UIImage* bookImageDetail;
#property (strong, nonatomic) IBOutlet UIImageView *bookImageDetailView;
viewDidLoad of my .m file of the secondViewController (ParseBookDetailViewController)
NSLog(#"%#",bookImageDetail);
[bookImageDetailView setImage:bookImageDetail];
As I said: in the original View Controller the Image is correct. I can log it and even set it there to an ImageView. But in the second view controller there is (null).
Glad for help, thanks..
EDIT:
This is the code from my cellForRowAtIndexPath.
PFObject *tempObject = [totalStrings objectAtIndex:indexPath.row];
//cell.textLabel.text = [tempObject objectForKey:#"bookTitle"];
cell.titleTextField.text = [tempObject objectForKey:#"bookTitle"];
cell.bookAutor1Label.text = [tempObject objectForKey:#"bookAutor1"];
cell.isbnLabel.text = [tempObject objectForKey:#"bookISBN"];
cell.statusLabel.text = [tempObject objectForKey:#"bookStatus"];
cell.yearLabel.text = [tempObject objectForKey:#"BookDate"];
if ([cell.statusLabel.text isEqualToString:#"nicht verfügbar"]) {
cell.statusLabel.tintColor = [UIColor redColor];
cell.dotImageIcon.image = [UIImage imageNamed:#"dot_red.png"];
}else if ([cell.statusLabel.text isEqualToString:#"verfügbar"]){
cell.statusLabel.tintColor = [UIColor greenColor];
cell.dotImageIcon.image = [UIImage imageNamed:#"dot_green.png"];
}
PFFile *eventImage = [tempObject objectForKey:#"bookImage"];
if(eventImage != NULL)
{
[eventImage getDataInBackgroundWithBlock:^(NSData *imageData, NSError *error)
{
UIImage *thumbnailImage = [UIImage imageWithData:imageData];
prepareForSegueImage = thumbnailImage;
[cell.bookImageView setImage:thumbnailImage];
}];
}
At the bottom I have set the Image (that works) in a new property (prepareForSegueImage). NOW its loaded.
And now I've tried to pass only the prepareForSegueImage to the new view controller, but it actually doesn't pass the one from the cell, it passes the last one loaded in the whole table view from the first view controller.
I'm guessing:
[eventImage getDataInBackgroundWithBlock:^(NSData *imageData, NSError *error)
{
UIImage *thumbnailImage = [UIImage imageWithData:imageData];
pbdtvc.bookImageDetail = thumbnailImage;
}];
is an asynchronous request happening in the background, that will take some time to return. But your code is expecting it to happen instantly.
Load the image first and call the segue in the completion block,
EDIT
If it works in the first viewController (meaning you have the image) why are you requesting it again? why not simply pass the image you already have?
EDIT 2:
As explained in my comment, your variable is being overwritten each time the cellForRowAtindexPath is called. You could either wrap that code in an if statement, checking for an index or a certain image etc. or you can use the below code to get a specific cell and pull the image form that.
[tblView cellForRowAtIndexPath: [NSIndexPath indexPathForRow:2 inSection:0]];
If you have the image downloaded and have a relatively easy means of getting a reference to it you should not be downloading it again. This could cost the user on a 3G data plan as well as waste resources.
This happens because you are using the getDataInBackgroundWithBlock, which is an asynchronous call. As this happens in a background thread, it is probably not finished when you segue to the other viewcontroller.
You said it works fine in the first controller. I assume this means you have already downloaded the image. Put that in a property instead, and send this property to the next controller.

Activity Indicator in a list

I am creating a real estate app. I have a screen which displays a listing of all entries with a thumbnail and a little text on the side. These I have loaded from the server when the app launched. Each entry can have up to 5 photos, which I do not pre-load for obvious reasons. My issue is this… when the user selects an entry, the app downloads the larger photos from the server. Depending on circumstances this can take a few seconds. Right now the app just hangs for those few seconds. I don't know of any practical way to use an activity indicator in a list. A header space just seems like wasted space to use only to display"Loading…". Anyone have any ideas on what I can do to let the user know that loading is in progress?
Clarification: Once an entry is selected from the list, I load up another Table View Controller which has the photos in its list of selections. I currently load the photos in the ViewDidLoad using
NSData *myPhoto = [[NSData alloc] initWithContentsOfURL:[NSURL URLWithString:myURL]];
You can:
Use UIActivityIndicatorView to show a spinning activity indicator in the precise spot where the image will eventually be loaded.
In a separate queue download the image. While the below code uses GCD, it's actually much better to use NSOperationQueue because on a slow network, using GCD can consume all of the available worker threads, detrimentally affecting performance on the app. A NSOperationQueue with a reasonable maxConcurrentOperationCount (such as 4 or 5) is much better.
When the download is complete, dispatch the updating of the UI back to the main queue (e.g. turn off the activity indicator and set the image).
This is sample code from a gallery app that shows how you might do it. This is probably more complicated than you need and might be hard to repurpose via cut-and-paste, but the loadImage method shows the basic elements of the solution.
#interface MyImage : NSObject
#property (nonatomic, strong) NSString *urlString;
#property (nonatomic, strong) UIImageView *imageView;
#property (nonatomic, strong) UIActivityIndicatorView *activityIndicator;
#property (nonatomic, strong) UIView *view;
#property BOOL loading;
#property BOOL loaded;
#end
#implementation MyImage
// I find that I generally can get away with loading images in main queue using Documents
// cache, too, but if your images are not optimized (e.g. are large), or if you're supporting
// older, slower devices, you might not want to use the Documents cache in the main queue if
// you want a smooth UI. If this is the case, change kUseDocumentsCacheInMainQueue to NO and
// then use the Documents cache only in the background thread.
#define kUseDocumentsCacheInMainQueue NO
- (id)init
{
self = [super init];
if (self)
{
_view = [[UIView alloc] initWithFrame:CGRectMake(0.0, 0.0, IMAGE_WIDTH, IMAGE_HEIGHT)];
_imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0.0, 0.0, IMAGE_WIDTH, IMAGE_HEIGHT)];
_imageView.contentMode = UIViewContentModeScaleAspectFill;
_imageView.clipsToBounds = YES;
[_view addSubview:_imageView];
_loading = NO;
_loaded = NO;
}
return self;
}
- (void)loadImage:(dispatch_queue_t)queue
{
if (self.loading)
return;
self.loading = YES;
ThumbnailCache *cache = [ThumbnailCache sharedManager];
if (self.imageView.image == nil)
{
// I've implemented a caching system that stores images in my Documents folder
// as well as, for optimal performance, a NSCache subclass. Whether you go through
// this extra work is up to you
UIImage *imageFromCache = [cache objectForKey:self.urlString useDocumentsCache:kUseDocumentsCacheInMainQueue];
if (imageFromCache)
{
if (self.activityIndicator)
{
[self.activityIndicator stopAnimating];
self.activityIndicator = nil;
}
self.imageView.image = imageFromCache;
self.loading = NO;
self.loaded = YES;
return;
}
// assuming we haven't found it in my cache, then let's see if we need to fire
// up the spinning UIActivityIndicatorView
if (self.activityIndicator == nil)
{
self.activityIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
self.activityIndicator.center = CGPointMake(self.view.frame.size.width / 2.0, self.view.frame.size.height / 2.0);
[self.view addSubview:self.activityIndicator];
}
[self.activityIndicator startAnimating];
// now, in the background queue, let's retrieve the image
dispatch_async(queue, ^{
if (self.loading)
{
UIImage *image = nil;
// only requery cache for Documents cache if we didn't do so in the main
// queue for small images, doing it in the main queue is fine, but apps
// with larger images, you might do this in this background queue.
if (!kUseDocumentsCacheInMainQueue)
image = [cache objectForKey:self.urlString useDocumentsCache:YES];
// if we haven't gotten the image yet, retrieve it from the remote server
if (!image)
{
NSData *data = [[NSData alloc] initWithContentsOfURL:[NSURL URLWithString:self.urlString]];
if (data)
{
image = [UIImage imageWithData:data];
// personally, I cache my image to optimize future access ... you might just store in the Documents folder, or whatever
[cache setObject:image forKey:self.urlString data:data];
}
}
// now update the UI in the main queue
dispatch_async(dispatch_get_main_queue(), ^{
if (self.loading)
{
[self.activityIndicator stopAnimating];
self.activityIndicator = nil;
self.imageView.image = image;
self.loading = NO;
self.loaded = YES;
}
});
}
});
}
}
// In my gallery view controller, I make sure to unload images that have scrolled off
// the screen. And because I've cached the images, I can re-retrieve them fairly quickly.
// This sort of logic is critical if you're dealing with *lots* of images and you want
// to be responsible with your memory.
- (void)unloadImage
{
// remove from imageview, but not cache
self.imageView.image = nil;
self.loaded = NO;
self.loading = NO;
}
#end
By the way, if the image you're downloading is in a UIImageView in a UITableViewCell the final update back to the table might want to do something about checking to see if the cell is still on screen (to make sure it wasn't dequeued because the UITableViewCell scrolled off the screen). In that case, the final UI update after successful download of the image might do something like:
dispatch_async(dispatch_get_main_queue(), ^{
// if the cell is visible, then set the image
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
if (cell)
{
cell.imageView.image = image;
}
});
Note, this is using the UITableView method cellForRowAtIndexPath, which should not be confused with the UITableViewController method tableView:cellForRowAtIndexPath.
For one of my projects i used this custom class for UIImageView:
https://github.com/nicklockwood/AsyncImageView
Small tutorial is located here: http://www.markj.net/iphone-asynchronous-table-image/
With just few lines of code i managed to implement asynchronous loading of images, caching etc. Just give it a look.

showing progress on imageview on customcell on uitableview

I followed the codes here https://stackoverflow.com/a/7212943/711837 to get me started to show an indicator my app trying to download images from a particular website. The scenario is:
i have a tableview with many custom cells, the custom cells has 2 labels and a imageview.
i have a NSURL to download the contents that will fill up the labels and then a separate class that will download the images to be filled into the UIImageView. The code for the spinner and the downloading of images are:
resultArray = [[NSArray alloc] initWithArray:response];
[self downloadImageFromInternet:#"http://static.colourlovers.com/images/shapes/0/7/7090/50x50.png"];
//spinner for the download
spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
spinner.frame = CGRectMake(0, 0, 24, 24);
custcell.accessoryView = spinner;
[spinner startAnimating];
///[spinner release];
[self.thetableView reloadData];
and then i call the [spinner stopAnimating] at the finish downloading method of the class but somehow, the spinners just don't animate, or appear for the matter! am i missing something? or is there somewhere i can refer to? my aim is to show the UIIndicatorView at the place of the UIImageView then after loading, the imageview takes over the same position and this is on every cell.
UPDATED added the methods
-(void) downloadImageFromInternet:(NSString*)urlToImage{
// Create a instance of InternetImage
asynchImage = [[DownloadThumb alloc] initWithUrl:urlToImage];
// Start downloading the image with self as delegate receiver
[asynchImage downloadImage:self];
}
-(void) internetImageReady:(DownloadThumb*)downloadedImage{
// The image has been downloaded. Put the image into the UIImageView
[arrayImg addObject:downloadedImage.Image];
[spinner stopAnimating];
[self.thetableView reloadData];
}
-(void)downloadImage:(id)delegate{
m_Delegate = delegate;
NSURLRequest *imageRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:ImageUrl]
cachePolicy:NSURLRequestReturnCacheDataElseLoad timeoutInterval:60.0];
imageConnection = [[NSURLConnection alloc] initWithRequest:imageRequest delegate:self];
if(imageConnection)
{
workInProgress = YES;
m_ImageRequestData = [[NSMutableData data] retain];
}
}
You should really initiate the spinner when you create the cell.
Then you should check in cellForRowAtIndexPath if it should be visible or not.
Finally, when you need to display it, you can use [tableView reloadData] to display or remove it.

Asynchronously loading sound resources in viewDidLoad crashes

All,
I am attempting to load a set of sounds asynchronously when I load a UIViewController. At about the same time, I am (occasionally) also placing a UIView on the top of my ViewController's hierarchy to present a help overlay. When I do this, the app crashes with a bad exec. If the view is not added, the app does not crash. My ViewController looks something like this:
- (void)viewDidLoad
{
[super viewDidLoad];
__soundHelper = [[SoundHelper alloc] initWithSounds];
// Other stuff
}
- (void)viewDidAppear:(BOOL)animated
{
// ****** Set up the Help Screen
self.coachMarkView = [[FHSCoachMarkView alloc] initWithImageName:#"help_GradingVC"
coveringView:self.view
withOpacity:0.9
dismissOnTap:YES
withDelegate:self];
[self.coachMarkView showCoachMarkView];
[super viewDidAppear:animated];
}
The main asynchronous loading method of SoundHelper (called from 'initWithSounds') looks like this:
// Helper method that loads sounds as needed
- (void)loadSounds {
// Run this loading code in a separate thread
NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
NSBlockOperation *loadSoundsOp = [NSBlockOperation blockOperationWithBlock:^{
// Find all sound files (*.caf) in resource bundles
__soundCache = [[NSMutableDictionary alloc]initWithCapacity:0];
NSString * sndFileName;
NSArray *soundFiles = [[NSBundle mainBundle] pathsForResourcesOfType:STR_SOUND_EXT inDirectory:nil];
// Loop through all of the sounds found
for (NSString * soundFileNamePath in soundFiles) {
// Add the sound file to the dictionary
sndFileName = [[soundFileNamePath lastPathComponent] lowercaseString];
[__soundCache setObject:[self soundPath:soundFileNamePath] forKey:sndFileName];
}
// From: https://stackoverflow.com/questions/7334647/nsoperationqueue-and-uitableview-release-is-crashing-my-app
[self performSelectorOnMainThread:#selector(description) withObject:nil waitUntilDone:NO];
}];
[operationQueue addOperation:loadSoundsOp];
}
The crash seems to occur when the block exits. The init of FHSCoachMarkView looks like this:
- (FHSCoachMarkView *)initWithImageName:(NSString *) imageName
coveringView:(UIView *) view
withOpacity:(CGFloat) opacity
dismissOnTap:(BOOL) dismissOnTap
withDelegate:(id<FHSCoachMarkViewDelegate>) delegateID
{
// Reset Viewed Coach Marks if User Setting is set to show them
[self resetSettings];
__coveringView = view;
self = [super initWithFrame:__coveringView.frame];
if (self) {
// Record the string for later reference
__coachMarkName = [NSString stringWithString:imageName];
self.delegate = delegateID;
UIImage * image = [[UIImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:imageName ofType:#"png"]];
// ****** Configure the View Hierarchy
UIImageView *imgView = [[UIImageView alloc] initWithImage:image];
[self addSubview:imgView];
[__coveringView.superview insertSubview:self aboveSubview:__coveringView];
// ****** Configure the View Hierarchy with the proper opacity
__coachMarkViewOpacity = opacity;
self.hidden = YES;
self.opaque = NO;
self.alpha = __coachMarkViewOpacity;
imgView.hidden = NO;
imgView.opaque = NO;
imgView.alpha = __coachMarkViewOpacity;
// ****** Configure whether the coachMark can be dismissed when it's body is tapped
__dismissOnTap = dismissOnTap;
// If it is dismissable, set up a gesture recognizer
if (__dismissOnTap) {
UITapGestureRecognizer * tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self
action:#selector(coachMarkWasTapped:)];
[self addGestureRecognizer:tapGesture];
}
}
return self;
}
I have tried invoking the asynchronous block using both NSBlockOperation and dispatch_async and both have had the same results. Additionally, I've removed the aysnch call altogether and loaded the sounds on the main thread. That works fine. I also tried the solution suggested by #Jason in: NSOperationQueue and UITableView release is crashing my app but the same thing happened there too.
Is this actually an issue with the view being added in FHSCoachMarkView, or is it possibly related to the fact that both access mainBundle? I'm a bit new to asynch coding in iOS, so I'm at a bit of a loss. Any help would be appreciated!
Thanks,
Scott
I figured this out: I had set up a listener on the SoundHelper object (NSUserDefaultsDidChangeNotification) that listened for when NSUserDefaults were changed, and loaded the sounds if the user defaults indicated so. The FHSCoachMarkView was also making changes to NSUserDefaults. In the SoundHelper, I was not properly checking which defaults were being changed, so the asynch sound loading method was being called each time a change was made. So multiple threads were attempting to modify the __soundCache instance variable. it didn't seem to like that.
Question: Is this the correct way to answer your own question? Or should I have just added a comment to the question it self?
Thanks.

using NSInvocationOperation for UITableViewCell causes some UIImageViews to display the wrong image? Possible re-entrant issue?

I'm trying to keep my UITableView scrolling smoothly while going through about 700 pictures that are downloaded from the internet, cached (to internal storage) and displayed on each cell of the table. My code so far seems fine as far as scrolling performance. However, I noticed that sometimes, if the connection is being crappy or If I scroll really fast, a cell will display the wrong picture (that of another cell) for maybe about 1/2 a sec and then update to the image it is supposed to display.
I suspect 2 things so far:
A-
I might have a reentrancy issue from the point where my NSInvocationOperation calls back into the main thread with [self performSelectorOnMainThread:] to the point where the selector in the main thread gets executed. Though I don't really spot any shared variables.
B-
Some sort of race between the main thread and the NSInvocationOperation? Like:
1 main thread calls cacheImageFromURL
2 inside this call, UIImage spans the worker thread
3 worker thread is almost done and gets to call performSelectorOnMainThread
4 the cell in question is dequeued to be reused at this point, so main thread calls cahceImageFromURL again for a new image.
5 inside this call, UIImage stops the NSOPerationQueue which causes the previous NSInvocationOperation thread to die.
6 BUT, the thread had already called performSelectorOnMainThread
7 so the selector gets excited causing the old image to load.
8 immediately after this, the recently spawned thread is done fetching the new image and calls performSelectorOnMainThread again, causing the update to the right image.
If this is the case, I guess I'd need to set a flag on entry to the cacheImageFromURL method so that the worker thread code doesn't call performSelectorOnMainThread if there's another thread (the main one) already inside cacheImageFromURL?
Here's my code for my UIImageView subclass, which each cell in the table uses:
#implementation UIImageSmartView
//----------------------------------------------------------------------------------------------------------------------
#synthesize defaultNotFoundImagePath;
//----------------------------------------------------------------------------------------------------------------------
#pragma mark - init
//----------------------------------------------------------------------------------------------------------------------
- (void)dealloc
{
if(!opQueue)
{
[opQueue cancelAllOperations];
[opQueue release];
}
[super dealloc];
}
//----------------------------------------------------------------------------------------------------------------------
#pragma mark - functionality
//----------------------------------------------------------------------------------------------------------------------
- (bool)cacheImageFromURL:(NSString*)imageURL
{
/* If using for the first time, create the thread queue and keep it
around until the object goes out of scope*/
if(!opQueue)
opQueue = [[NSOperationQueue alloc] init];
else
[opQueue cancelAllOperations];
NSString *imageName = [[imageURL pathComponents] lastObject];
NSString* cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
NSString *cachedImagePath = [cachePath stringByAppendingPathComponent:imageName];
/* If the image is already cached, load it from the local cache dir.
Else span a thread and go get it from the internets.*/
if([[NSFileManager defaultManager] fileExistsAtPath:cachedImagePath])
[self setImage:[UIImage imageWithContentsOfFile:cachedImagePath]];
else
{
[self setImage:[UIImage imageWithContentsOfFile:self.defaultNotFoundImagePath]];
NSMutableArray *payload = [NSMutableArray arrayWithObjects:imageURL, cachedImagePath, nil];
/* Dispatch thread*/
concurrentOp = [[NSInvocationOperation alloc] initWithTarget:self selector:#selector(loadURI:) object:payload];
[opQueue addOperation: concurrentOp];
[concurrentOp release];
}
return YES;
}
//----------------------------------------------------------------------------------------------------------------------
/* Thread code*/
-(void)loadURI:(id)package
{
NSArray *payload = (NSArray*)package;
NSString *imageURL = [payload objectAtIndex:0];
NSString *cachedImagePath = [payload objectAtIndex:2];
/* Try fetching the image from the internets.
If we got it, write it to disk. If fail, set the path to the not found again.*/
UIImage *newThumbnail = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:imageURL]]];
if(!newThumbnail)
cachedImagePath = defaultNotFoundImagePath;
else
[UIImagePNGRepresentation(newThumbnail) writeToFile:cachedImagePath atomically:YES];
/* Call to the main thread - load the image from the cache directory
at this point it'll be the recently downloaded one or the NOT FOUND one.*/
[self performSelectorOnMainThread:#selector(updateImage:) withObject:cachedImagePath waitUntilDone:NO];
}
//----------------------------------------------------------------------------------------------------------------------
- (void)updateImage:(NSString*)cachedImagePath
{
[self setImage:[UIImage imageWithContentsOfFile:cachedImagePath]];
}
//----------------------------------------------------------------------------------------------------------------------
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
// Return YES for supported orientations
return (interfaceOrientation == UIInterfaceOrientationPortrait);
}
#end
And the way this UIImage is used is in the context of cellForRowAtIndexPath, like so:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UIImageSmartView *cachedImage;
// and some other stuff...
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil)
{
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:CellIdentifier] autorelease];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell.selectionStyle = UITableViewCellSelectionStyleGray;
// some labels and tags stuff..
cachedImage = [[UIImageSmartView alloc] initWithFrame:CGRectMake(5, 5, 57, 80)];
cachedImage.contentMode = UIViewContentModeCenter;
cachedImage.defaultNotFoundImagePath = [[NSBundle mainBundle] pathForResource:#"DefaultNotFound" ofType:#"png"];
cachedImage.tag = PHOTO_TAG;
[cell.contentView addSubview:cachedImage];
[cell.contentView addSubview:mainLabel];
[cell.contentView addSubview:secondLabel];
}
else
{
cachedImage = (UIImageSmartView*)[cell.contentView viewWithTag:PHOTO_TAG];
mainLabel = (UILabel*)[cell.contentView viewWithTag:MAINLABEL_TAG];
}
// Configure the cell...
NSString *ImageName = [[[self.dbData objectAtIndex:indexPath.row] objectAtIndex:2]
stringByReplacingOccurrencesOfString:#".jpg"
withString:#"#57X80.png"];
NSString *imageURL = [NSString stringWithFormat:#"www.aServerAddress.com/%#/thumbnail5780/%#",
self.referencingTable,
ImageName];
[cachedImage cacheImageFromURL:imageURL];
mainLabel.text = [[self.dbData objectAtIndex:indexPath.row] objectAtIndex:0];
return cell;
}
The problem is reuse of cell, one cell makes the request of various images at same time, and is displaying when each one is downloaded, i know that you are canceling operations queue but as the processing caller is synchronous the operation continues the execution. I suggest try to save the indexPath of the request, and match it with the index path of the cell before set the UIImage.
D33pN16h7 is right in that the problem was cell reuse. However, instead of trying to make the indexPath thread-safe through an NSURLConnection, I decided to reimplement the whole thing by moving the NSOperationQueue into the UITableViewController code and having the concurrent imageView class be actually a proper subclass of NSOperation (since I was using NSOperationInvocation in the first place to try and avoid the full-fledged NSOperation subclass).
So now, the table controller manages it's own NSOperationQueue, the operations are subclasses of NSOperation and I can cancel them from the table controller code as the table view scrolls past them. And everything works fast and nice.

Resources