I have a collectionView with UICollectionViewFlowLayout with some reusability:
- (CategoryCell*)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
CategoryCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"CategoryCell" forIndexPath:indexPath];
[cell.actIn startAnimating];
cell.cellElementRepresentation = [self.localCategoriesObjects objectAtIndex:indexPath.row];
[cell tryToShow]; //this is a important method to my problem
return cell;
}
I commented for you tryToShow method. Becouse i think this is a problem. When i scroll to the not visible element in the list I could see "old" elemnts first then new element. Why is that? My tryToShow method is basically download manager. I try to describe it in comment. Here is the code:
-(void)tryToShow {
NSString* imageFile = self.linkToImageFileSavedOnDisk //This is my local file saved in documentsDirectory.
if([[NSFileManager defaultManager] fileExistsAtPath:imageFile]) {
[self.overlayButton setBackgroundImage:[UIImage imageWithContentsOfFile:imageFile] forState:UIControlStateNormal];
[self disableActIn]; //Here i stop and remove activity indicator.
} else {
// Here i fire NSURLConnection and try to download. When NSURLConnection finished i put image to overlayButton.
}
}
Here is delegate method:
-(void)connectionDidFinishLoading:(NSURLConnection *)connection {
//Here do some stuff to convert Data into UIImage... not important.
[self.overlayButton setBackgroundImage:object forState:UIControlStateNormal];
}
The overlayButton is a button on the UICollectionViewCell. I have a custom class for it.
I can't figure it out. How it's possible to image from indexPath.row (for example 1) is visible after scroll ? The image is replaced by new one right after new is successfully download, but this is to late.
As the OP mention, the problem seems to be that you are setting the backgroundImage too late. So I would try this modifications:
First, clear your cell before setting the new values. Since you are reusing the cell, the old values (text and images) will still be present in the cell subviews. Setting those values to nil will clear your cell.
Implement a mechanism to validate or invalidate the result from the image request, since could get the result too late, maybe is no longer needed. Worst, you can set an image no longer correspondent with your data. You should check, before setting a downloaded image, that this image is consistent with the current shown data.
Wait a little before start the image request If you have a large number of cells, you should think that when the user quickly scroll the collection, you can start lots of requests for images that may never be displayed. I would avoid this behavior by waiting a little before start a new request, and checking if the cell has been reused again.
Use the NSURLRequest and configure the cachePolicy attribute as NSURLRequestReturnCacheDataElseLoad. This will store the responses for the images, there is no need to save the images explicitly save the image and check it's existence.
I think you need to set the background image of self.overlayButton to nil in the else clause where you start the web request.
[self.overlayButton setBackgroundImage:nil forState:UIControlStateNormal];
That way the cell won't appear to have an image loaded already when the user scrolls to it. Or, if it's not loading quickly enough (which it should), you could possibly set the background image to nil in the collectionView:didEndDisplayingCell:forItemAtIndexPath: delegate method instead.
But a problem I see is that you will be creating duplicate NSURLConnection objects every time tryToShow is called, i.e., the user scrolls down then revisits the cells at the top.
As #lucaslt89 suggested, you could create a dictionary of cells instead of using the default dequeuing. If you do it this way, make sure you have a property for the URL connection object on the cell, and you check for it before starting a new one.
Or, you may want to create a dictionary of any NSURLConnection objects you create in the view controller, and move the web request code into the view controller. If a web request is needed, the view controller would check if there was already a web request running in the dictionary first. When the request is finished, the request object would be removed from the dictionary. This would be the desirable method because the cell isn't responsible for creating its own data. In UICollectionView the cells have to quickly change roles and it's not wise to associate a particular cell with some data unless there are really few cells.
Either way, you'll avoid creating multiple URL requests for the same image.
Hope this helps!
Related
SDWebImage is an extension to UIImageView. There is a method:
sd_setImageWithURL(NSURL(string: url), placeholderImage: placeholder, options: options, completed: { image, error, cache, url in
//do sth after it is completed
})
But in completed block I cannot decide whether to allow SDWebImage assign the image or not. It is assigned automaticaly without asking developer about permission to do this. The problem arise when I assign the image to UIImageView within UITableViewCell while scrolling the table.
It is obvious that before it finishes downloading, that cell may be reused for different row, and finally there may be a different image.
How to fix it using this library?
In following question: Async image loading from url inside a UITableView cell - image changes to wrong image while scrolling, there is an answer that SDWebImage solve this problem, but it is not true, or I missed sth.
This shouldn't be necessary. If you look inside the implementation of the UIImageView category you will see this:
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {
[self sd_cancelCurrentImageLoad];
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
What this shows you is that the minute you call setImage it cancels the download associated with the image view instance. Since UITableView reuses its views this should not be an issue. See how they associate the imageView and URL together.
Your issue is probably the fact that you are not resetting your imageView's image property to nil everytime. So inside cellForRow you probably want:
cell.imageView.image = nil
cell.imageView.sd_setImageWithURL(...)
When you start a new load the image view cancels any current load. So, if the image view is reused this problem is avoided.
If you don't have a new image to set for some reason then you can call sd_cancelCurrentImageLoad and explicitly set your placeholder image.
I have a UITableView that loads its array items fine on the first call. However when i refresh the tableview it crashes because the delegate method cellForRowAtIndexPath gets called to early. I have an integer which represents the index of my main data array and this gets reset to 0 in my refresh button. However its crashing because its trying to reload the data before its been reset. I would normally use indexPath.row as my index however the array is complicated and the indexPath.row will not match up to what i want to show for each cell. Heres some code, any help is appreciated.
This gets called when i pull down to refresh AND in viewDidLoad to prepare the data
- (IBAction)refresh:(id)sender {
itemIndexer = 0;
[sender beginRefreshing];
[self loadData];
}
Part of my loadData method
-(void) loadData {
dispatch_queue_t backgroundQueue = dispatch_queue_create("com.Foo.myqueue", 0);
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_async(backgroundQueue, ^{
[self downloadData];
dispatch_async(mainQueue, ^{
[self.tableView reloadData];
[_rcRefresh endRefreshing];
});
});
In viewDidLoad i call to initially load the tableview:
[self refresh:_rcRefresh];
I am getting a index outside bounds of array error. Which i have used breakpoints to determine why, the reason is simply because the refresh isn't getting called at all otherwise itemIndexer would be set to 0. Instead its a number 1 greater than the array size. If its really necessary i can post cellForRowAtIndexPath however I'm unsure if you need it if it works on the first call. So to summarise the first call to the tableview works and the data is loaded fine however the refresh causes the index to be outside the bounds of the array as if refresh: is never called.
For what you say, I can only try guessing:
Your numberOfSectionsInTableView/numberOfRowsInSection is returning a wrong number.
If [self downloadData] is asynchronous (you are making a server request and not waiting for the response), you should reloadData once you have the data.
The data you download is not merged properly with the data you already have.
Some more code (numberOfSectionsInTableView, numberOfRowsInSection, cellForRowAtIndexPath, downloadData) would definitely help.
Couple of points for clarity...
As indicated by #k20, it is not that tableView:cellForRowAtIndexPath: is called too early, but that you need to better manage your download data once your asynchronous process / method has completed.
tableView:cellForRowAtIndexPath: is a UITableView data source method, not a delegate method. It is worth mentioning this pedantic detail because it may help you or others better understand the code you are writing. ;)
The table view method calls are what they are - as I understand it, all table view methods are called in order, each time a UITableViewController is init or awakeFromNib. Those that you must override (tableView:numberOfRowsInSection: & tableView:cellForRowAtIndexPath:), and those that you choose to override will still execute in that same order.
Therefore a more appropriate title for your question might be... "How to update a UITableView with data from a download on an asynchronous thread."
Again #k20 is pointing you to the correct solution. Have you attempting placing these two lines of code...
[self.tableView reloadData];
[_rcRefresh endRefreshing];
within your async call, instead of back in the main queue?
It may be that your code as written is executing like this...
Prepare local variables for dispatch_q_t;
Commence download process;
reload data for table view;
end the refresh [_rcRefresh endRefreshing];
depending on time it takes, then finish download process;
Where you obviously would like to execute like this...
Prepare local variables for dispatch_q_t;
Commence download process;
depending on time it takes, finish download process;
reload data for table view;
end the refresh [_rcRefresh endRefreshing];
Try my suggestion and let me know how you go. Hope that helps.
I'm using a UIPageViewController for displaying images, and I download the images asynchronously and throw them into the gallery when they're ready (but you're still able to go to the page in the gallery where the image will be).
My issue is that, when the image does load, I need to supply it for the view controller it corresponds to. Right now, I add it to an NSCache instance, and when UIPageViewController's data source method viewControllerAfterViewController: is called, I check if the image for the view controller being requested has already been downloaded (is in the cache) and if it is, I call initWithImage: on the specific view controller, passing the downloaded image.
My issue is with when viewControllerAfterViewController: is called and the image hasn't been downloaded yet. Right now I just load the view controller without the image, and when the imageDidFinishDownloading: delegate method is called, I try to supply the view controller with the image.
However, it seems that even though UIPageViewController asks for the next view controller the previous gets displayed (i.e.: when I get to the 3rd page, it requests the 4th page's view controller) I'm not able to access this requested view controller that I hand off.
If I access pageViewController.viewControllers, the count of the NSArray is never more than 1. So even though it seems like it should have 2 (the currently showing view controller and the next one that it requested), it only ever has one, the currently visible view controller.
The problem is that I need the other one. The image finishes downloading, and I really only have two options.
Give it to my NSCache, so when the init method is called for the view controller, it is handed off.
In the event that it has already been inited, hand it off to the view controller that's already been inited.
But 1 doesn't always work, as sometimes the init method is called when there's no image yet (it hasn't finished downloading), so we'd sometimes depend on 2, but pageViewController.viewControllers only holds the current visible view controller.
So there's basically this third situation where the view controller has already been inited without an image, and the image finishes downloading and I want to assign it to that view controller, but I'm currently on the one before it, and my only reference to the view controllers in the UIPageViewController is on the current visible one.
How do I handle 3? I need to assign something to a view controller but I can't find any way to access that view controller.
I'd use something like a future from PMConcurrency to do this. A future is just an object that promises to return your image as soon as it's available. So by giving the future to your view controller instead of the image, you don't need to worry about when the image arrives.
Your view controller's initializer would look something like this:
- initWithImageFuture:(PMFuture *)future
{
if (self = [super init]) {
[[future onMainThread] onComplete:^(id result, NSError *error) {
if (!error) {
_image = result;
}
}];
}
return self;
}
By adding an onComplete block to the future, the view controller will receive the image as soon as it's available (immediately if the image is already downloaded). Futures run in the background by default, so you'd use onMainThread to have the result returned on the main thread.
So instead of populating your cache with images, you'd populate it with image futures. There are several ways to create futures, but the block-based one is pretty simple:
PMFuture *imageFuture = [PMFuture futureWithBlock:^id{
UIImage *image = ...; // Your image fetching code here
return image;
}];
[myCache setObject:imageFuture forKey:myImageKey];
I have a similar sort of function but I load the image in the view controller that is displaying the image (the VC handed to the UIPageViewController). In your context I guess I'd had the image ID info and the NSCache object to the VC. That isn't what you want to do of course, but just something to think about.
Originally I tried to handle image loading in the parent like you are. As I recall, I kept a weak pointer to the target view controller. When the image arrived if the VC was still alive I could set the UIImageView property.
I'm displaying lots of images loaded directly from my app (not downloaded). My table view is slow when I scroll it the first time. It becomes smooth after all my cell has been displayed. I don't really know why.
I have an array of UIImage that I'm loading in the viewDidLoad. Then in my tableview delegate I just get the image at a given index path and set it to an UIImageView of my cell.
Do you know how I can improve performances ?
just to share I have fixed and it worked very well Steps I followed.
1) Set the performSelectorInBAckground function with Cell as parameter passed that holds the scroll view or uiview to put many iamges.
2) In the background function load the image stored from application bundle or local file using imagewithContents of file.
3) Set the image to the imageView using this code.
//// Start of optimisation - for iamges to load dynamically in cell with delay , make sure you call this function in performSelectorinBackground./////
//Setting nil if any for safety
imageViewItem.image = nil;
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^{
UIImage *image = // Load from file or Bundle as you want
dispatch_sync(dispatch_get_main_queue(), ^{
//Set the image to image view not, wither cell.imageview or [cell add subviw:imageview later ]
[imageViewItem setImage:image];
[imageViewItem setNeedsLayout];
});
});
//// End of optimisation/////
This will load all images dynamically and also scroll the table view quite smoothly than previous slow and jerky behaviour.
All the best
You can read the answer I have just submitted here:
Loading image from CoreData at cellForRowAtIndexPath slows down scrolling
The basic idea is to use Grand Central Despatch to move your table-view-image-getting code to a separate thread, filling in your cells back on the main thread as the images become available. Your scrolling will be super-smooth even if there's a delay loading the images into memory from the filesystem.
What I understand from your question is that your images are all set to go and that they are loaded into RAM (stored in an array which is populated in viewDidLoad). In this case, I would want to see the exact code in cellForRowAtIndexPath in order to help out. My instinct tells me that something is being done there that shouldn't be done on the main thread (as He Was suggests). The thing is - if it's only a fetch from an NSArray (worst case O(log(n))), you shouldn't be seeing a performance hit.
I know you're not downloading the images but I would still recommend to do ANY non-UI operation on a background thread. I wrote something that might help you out.
I'm having the following issue.
I'm writing a RSS reader using CoreData and Apple Recipes example as a guide. I have a refresh button that re-downloads the RSS and verify using NSFetchRequest if there is new data. Once I finish going over the elements I commit the changes via the NSManagedObjectContext save method.
After the context save, the tableView disappears!
I then decided to call reloadData on the tableView to reflect the changes. So, once the NSManagedObjectContext is saved I call:
[self performSelectorOnMainThread:#selector(updateTableItems) withObject:nil waitUntilDone:NO];
-(void) updateTableItems {
[self.tableView reloadData];
}
This action causes the cell to delete the data while scrolling, when I pop the view and go back, I see all the changes and everything is okay.
I also read in one of the threads that I should use NSFetchedResultsController and make sure the UITableView is the delegate, same issue as the previous one.
What am I doing wrong ?
Why can't I see the changes in place?
Why the cell's content is being deleted?
Thanks!
It sounds like you are using two or more context on separate threads. You commit the save on the background thread context but don't merge the changes with the context connected to the UI on the front thread. This causes the UI context to come out of sync with the store which cause table rows to disappear when you scroll.
When you pop the controller by leaving the view, the context is deallocated such that when you go back to the view a second time, you have a new context aware of the changes to the store.
To prevent this problem, call refreshObject:mergeChanges: on the front context immediately after you save the background context. Have the front context register for a NSManagedObjectContextDidSaveNotification from the background context
I have been having a similar issue and it was driving me crazy. Basically in my code there are loads of competing threads trying to update the same data at the same time (the data behind the table view) and I think this some how causes the UITableView to "blow up" and its delegate methods stop firing. (You can prove this by adding NSLog's into the delegate methods).
It happens totally randomly and is really difficult to replicate.
I tried all sorts to fix this but the only thing that seems to reliably ensure that it can't happen was completely recreating my UITableView everytime the data changed as below. (So basically change everywhere where you call [self.tableView reloadData] with the following)
// The UITableView may in rare circumstances all of a sudden failed to render
// correctly. We're not entirely sure why this happens but its something to
// do with multiple threads executing and updating the data behind the view
// which somehow causes the events to stop firing. Resetting the delegate and
// dataSource to self isn't enough to fix things however so we have to
// completely recreate the UITableView and replace the existing one.
UITableView* tempTableView = [[[UITableView alloc] initWithFrame:CGRectMake(0, 0, 320, 387)] autorelease];
tempTableView.separatorColor = [UIColor grayColor];
tempTableView.backgroundColor = [UIColor clearColor];
if (self.tableView != nil)
{
[tempTableView setContentOffset:self.tableView.contentOffset animated:NO];
}
tempTableView.delegate = self;
tempTableView.dataSource = self;
[tempTableView reloadData];
if (self.tableView != nil) {
[self.tableView removeFromSuperview];
self.tableView = nil;
}
[self.view addSubview:tempTableView];
self.tableView = tempTableView;
I know this isn't the ideal fix and doesn't really explain the issue but I think this is an iOS bug. Would welcome any other input however.