i'm pretty new in obj/Xcode, and i need to do a UIcollectionView how take images from url request.
I've try to catch the scroll event with scrollViewDidScroll, then call the my request function with the parameter page + 1 or -1; depending of the scroll.
But everytime i'm going on infinite loop, and my cell images donc stop loading, i think its because my way is wrong :
Catching the scroll action :
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;
{
[self reload];
}
The function reload send the request and set the imgs.
How can i load properly the pages, and not 1000times for every scrolling.
Sorry for my english, and thank you
You dont need to call reload like that. As you scroll, 'cellForItemAtIndexPath' is automatically called. If theres a previously used cell in the table (out of view) then the cell is re-used for the new position it is populating - hence 'dequeueReusableCellWithReuseIdentifier' (hope you're using that?!).
So what you need to do is have code on that method itself, that will automatically 'lazy load' the image you want. Heres a copy of a cellForItemOfIndexPath method that achieves just that - the image is populated when its downloaded.
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
VideoSearchCell *cell = [self.collectionView dequeueReusableCellWithReuseIdentifier:#"cell" forIndexPath:indexPath];
YoutubeSearchResult *cellSearchResult = [self.ytSearchResults objectAtIndex:indexPath.row];
if (cellSearchResult.thumbnailImage.image == nil)
{
//NSLog(#"loading image");
__weak VideoSearchCell *weakCell = cell;
__weak YoutubeSearchResult *weakResult = cellSearchResult;
NSURL *url = [NSURL URLWithString:cellSearchResult.thumbUrl];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
UIImage *placeholderImage = [UIImage imageNamed:#"your_placeholder"];
[cellSearchResult.thumbnailImage setImageWithURLRequest:request placeholderImage:placeholderImage success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image)
{
weakResult.thumbnailImage.image = image;
weakCell.imageView.image = image;
[weakCell setNeedsLayout];
} failure:nil];
} else {
if (cell.imageView.image != cellSearchResult.thumbnailImage.image)
{
cell.imageView.image = cellSearchResult.thumbnailImage.image;
}
}
return cell;
}
So to explain - VideoSearchCell is my custom cell type. Its fairly simple, it just has a UIImageView on it called 'imageView'. Ive also got an array called 'ytSearchResults' - this contains an array of objects and some contain images, and some dont. As the images download, the results also get populated to that array so I dont download them more than necessary.
The code checks to see if theres an image in that array. If not, it does the downloading code.
By the way, I use __weak references to properties, because of ARC and not wanting to accidentally retain the cell, or the search result. Not enough time to explain all that here though! Hope the code helps.
One more thing - setImageWithURLRequest is a method from the very handy and extremely popular library AFNetworking (2.0). Get hold of that as you will use it all the time.
I do not recommend to load images before cell is actually used, because you never know if user will see that cell. You can start image loading when
- collectionView:cellForItemAtIndexPath: is called, and before starting download request you must check if download of this image is in progress, otherwise you will load it more than once.
Related
I am trying to use UIImageView extension of AFNetworking2.6.3 to get images from a remote server. everything works fine, images have returned and rendered successfully. but I get a retain cycle warning in Xcode7.3.1: Capturing 'cell' strongly in this block is likely to lead to a retain cycle
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"cell"];
if(self.dataSourceModel) {
Model *model = self.dataSourceModel.items[indexPath.row];
NSURL *url = [NSURL URLWithString:model.imageURL];
NSURLRequest *theRequest = [NSURLRequest requestWithURL:url];
UIImage *placeholderImage = [UIImage imageNamed:#"placeholder"];
//I don't use __weak cell in the block
[cell.imageView setImageWithURLRequest:theRequest placeholderImage:placeholderImage success:^(NSURLRequest * _Nonnull request, NSHTTPURLResponse * _Nullable response, UIImage * _Nonnull image) {
cell.imageView.image = image; //get warning at this line
[cell setNeedsLayout];
} failure:nil];
}
return cell;
}
I can see since the cell instance has been captured in the block, so the block has the ownership of the cell instance(and cell.imageView as well). if the retain cycle really exists, the cell or imageView should have the ownership of the block.
but I have reviewed the UIImageView+AFNetworking.h and UIImageView+AFNetworking.m source code, the UIImageView doesn't have any property of blocks. the block is just a parameter of the method, not a instance variable. UITableViewCell doesn't have the ownership of the blocks either.
I even used Leaks to check, and Leaks didn't report any error.
So I believe there is no retain cycle here, but why I still get the Xcode warning here ?? If I use __weak cell__weak UITableViewCell *weakCell = cell;, the warning will go away. But I still wanna know:
does it have a retain cycle here?
do I really need to use __weak cell?
or maybe imageView or cell really has the ownership of the block which I didn't realize?
Any hints will help, thanks a lot.
There's certainly no retain cycle, and you can avoid the warning by omitting the code in the completion block. Taking a look at the AFNetworking imageView subclass (line 152), the culminating point of setImage... is to set the image view's image. There's need for your code to set it again, or to change the layout state.
There should also be no need for if (self.dataSourceModel). If the datasource is built right, we've answered the number of rows in tableView:numberOfRowsInSection:, and that answer is zero when the model is empty, avoiding the call to cellForRow... altogether.
I'm at this all afternoon and I can't figure it, so your help would be awesome! I have a custom UICollectionViewCell which I'm populating with images I create on the fly. The paths of the images are stored in Parse and after the query is done, I call [self.collectionView reloadData].
In my cellForItemAtIndexPath I have the following code:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
MovieCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:reuseIdentifier forIndexPath:indexPath];
// Configure the cell
NSSortDescriptor *sort = [NSSortDescriptor sortDescriptorWithKey:#"original_title" ascending:YES];
[self.moviesforWatchlist sortUsingDescriptors:[NSArray arrayWithObjects:sort, nil]];
cell.userInteractionEnabled = YES;
UIActivityIndicatorView *spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite];
[cell addSubview:spinner];
NSDictionary *movie = self.moviesforWatchlist[indexPath.row];
NSString *moviePosterPath = movie[#"poster_path"];
NSString *posterURL = [NSString stringWithFormat:#"http://image.tmdb.org/t/p/w342%#", moviePosterPath];
NSURL *posterImage = [NSURL URLWithString:posterURL];
UIImage *cachedImage = [self.imageCache objectForKey:posterImage];
if (cachedImage) {
cell.imageView.image = cachedImage;
}
else if (cell.imageView.image == nil) {
cell.imageView.image = [UIImage imageNamed:#"default_collectionviewcell.jpeg"];
dispatch_queue_t downloadQueue = dispatch_queue_create("get image data", NULL);
dispatch_async(downloadQueue, ^{
[spinner startAnimating];
NSData *imageData = [NSData dataWithContentsOfURL:posterImage];
UIImage *image = [UIImage imageWithData:imageData];
dispatch_async(dispatch_get_main_queue(), ^{
[spinner stopAnimating];
MovieCollectionViewCell *updateCell = (id)[collectionView cellForItemAtIndexPath:indexPath];
if (updateCell) {
updateCell.imageView.image = image;
[updateCell reloadInputViews];
[self.collectionView reloadItemsAtIndexPaths:[self.collectionView indexPathsForVisibleItems]];
}
});
[self.imageCache setObject:image forKey:posterImage];
});
}
[self.collectionView reloadItemsAtIndexPaths:[self.collectionView indexPathsForVisibleItems]];
return cell;
}
I'm calling the query method like this:
-(void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self getWatchlistData];
[self.collectionView reloadData];
}
This results in a flashing of images until the images are loaded on to UICollectionViewCells. I want the images displayed in my collectionviewcell to be in alphabetical order, so I'm sorting my array that holds the paths to do so, in cellforitemsatindexpath is this whats causing the flashing of images? Or is it something else? Your help is much appreciated!
There are a number of things I would change:
I wouldn't sort your array in cellForItemsAtIndexPAth. That method is called as each cell is displayed on screen, so you are potentially resorting it numerous times when that is not necessary. If your data is changing between one cell being created and the next, then the re-sorting is likely to mess up the link between the collectionView indexPath.row and your array index. I would sort the array when it is populated, perhaps in your viewWillAppear or getWatchlistData methods. If the data does change after this, use some of the other collectionView delegate/datasource methods to handle inserts/deletes/moves. But I don't think this is causing the flickering of images.
In the code which you post back to the main queue, you call the cellForItemAtIndexPath method, update the cell image and call reloadInputViews (which presumably reconfigures the cell in some way). But you then call reloadItemsAtIndexPaths for all visible cells. What this will do is cause your collectionView to re-call the cellForItemAtIndexPath datasource method for each visible item, causing the image for each to be reloaded from the cache. I think this is the cause of your flickering. I would just save the downloaded image to the cache, and then call reloadItemsAtIndexPaths for just the one indexPath that's affected. The cellForRowAtIndexPath datasource method will be called, and will recover the image from the cache and add it to the new cell.
Likewise, I think the reloadItemsAtIndexPaths at the end of your cellForItemAtIndexPath is superfluous and is likely to contribute to the flickering.
I think you are at risk of some cell reuse problems - for example, you add the spinner to every cell. But the dequeued cells might have been displayed before and therefore already have a spinner (albeit stopped and so probably hidden). If you scroll back and forth a lot, you might end up with cells with loads of spinners, wasting memory. I would give the spinner a tag before adding it as a subView, and then use viewWithTag to determine whether a spinner has already been added to the dequeued cells (if so, remove it). And only add the spinner if necessary (i.e. in your if ...image == nil clause).
Perhaps unlikely, but if the dequeued cell has a default image (so cell.imageView.image != nil) and is being reused for a cell for which the image has not yet been cached (so cachedImage == nil), then none of your dispatch methods will be executed, so the image will never be downloaded.
Sorry if that's a bit heavy, but I hope it's of use.
I have same issue, when any image got download then it flicks the view. I am going before like reload that cell, so it will call some delegate methods of collectionview. So i removed it and make a separate method for reload cell. Now when any new image got reload then i call call this method instead of reloadCellForIndexPath:. In custom method just copy paste code from your cellForItemAtIndexPath:. Assign cell in custom method.
CollectionPhotoItem cell = (CollectionPhotoItem)[self.collection cellForItemAtIndexPath:indexPath];
That's it. it will shows your cell without any flicks. Thanks.
try to use DLImageLoader
[[DLImageLoader sharedInstance] displayImageFromUrl:#"image_url_here"
imageView:#"UIImageView here"];
[[DLImageLoader sharedInstance] loadImageFromUrl:#"image_url_here"
completed:^(NSError *error, UIImage *image) {
if (error == nil) {
// if we have no any errors
} else {
// if we got an error when load an image
}
}];
I have an app where I load a lot of large images. When I lazy-load them, and even after the image has been loaded, the cell does not load them until I take my finger off the screen. I am calling my downloadImageForVisiblePaths function in the UIScrollViewDelegate methods scrollViewDidEndDragging and in scrollViewDidEndDecelerating apart from this, I am also setting the image in the UITableView's cellForRowAtIndexPath method like so:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
// Code to load reusable custom cell
CustomObject *object = (CustomObject*) [self.tableArray objectAtIndex: indexPath];
if(!object.mainImage){
[self downloadImageForIndexPath:indexPath];
cell.mainImageView.image = [UIImage imageNamed:#"placeholder"];
}else{
cell.mainImageView.image = object.mainImage;
}
return cell;
}
Where the downloadImageForIndexPath looks like this:
-(void) downloadImageForIndexPath:(NSIndexPath*) indexPath{
UIImage *loadedImage = [[UIImage alloc] init];
// take url and download image with dispatch_async
// Once the load is done, the following is done
CustomCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
cell.mainImageView.image = loadedImage;
CustomObject *object = (CustomObject*) [self.tableArray objectAtIndex: indexPath];
object.mainImage = loadedImage;
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableVIew reloadData];
});
}
I can't see where I am going wrong. I need the images to load even when the finger is on the screen. This behaviour is similar to how the images load on apps like Google+, Instagram or Facebook.
Any pointers will be much appreciated.
It's hard to tell since you didn't include all the code for downloadImageForIndexPath, but it looks like you are assigning an image to a cell from a background thread (you shouldn't touch UI controls from background threads). Also, if you'r updating cell directly, you don't need to call reloadData.
I would also suggest using SDWebImage for displaying remote images in a tableview.
Every time I scroll the TableView, my images gets messed up, mainly the first row. I realy don`t kwon what to do.
Here is the code:
- (UITableViewCell *) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *cellIdentifier = #"Cell";
BarCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
[cell.activityFotoBar startAnimating];
cell.activityFotoBar.hidesWhenStopped = YES;
if(!cell){
cell = [[BarCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
}
NSMutableDictionary *infoBar = [self.bares objectAtIndex:indexPath.row];
NSString *nomeImagem = [infoBar objectForKey:#"foto"];
NSURL *url = [NSURL URLWithString:nomeImagem];
NSURLRequest *requestImagem = [NSURLRequest requestWithURL:url];
[NSURLConnection sendAsynchronousRequest:requestImagem queue:[NSOperationQueue currentQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
if(connectionError == nil){
cell.imageViewImagemBar.image = [UIImage imageWithData:data];
[cell.activityFotoBar stopAnimating];
}
}];
cell.labelNomeBar.text = [infoBar objectForKey:#"nome"];
cell.labelEnderecoBar.text = [infoBar objectForKey:#"endereco"];
cell.labelAvaliacaoBar.text = [NSString stringWithFormat:#"Votos: %#", [infoBar objectForKey:#"votos"]];
return cell;
}
Thanks in advance!
The problem happens because the asynchronous image request finishes after your cell scrolls off the screen and gets reused. Downloads complete "out of order", contributing to a visual confusion. Essentially, some of the cells put up for reuse by scrolling, are still "hot", in the sense that their image load is in progress. Reusing such cell creates a race between the old and the new image downloads.
You should change the strategy that you use to load the images: rather than sending a request and "forgetting" it, consider using connectionWithRequest:delegate: method, storing the connection in the cell, and calling cancel on it when prepareForReuse method is called. This way your reused cells would be "cold".
You can use SDWebcache library. It contains a UIImageView category class which can load images from urls. I find it works well with tableviews
The code should "almost" work. You just need to fix one issue to get it working (though not optimally):
When the asynchronous request sendAsynchronousRequest:queue:completionHandler: has been finished, the completion handler will be called. Then, you need to retrieve the cell again from the table view specifying the indexPath at the time the request has been started.
You just need to capture the indexPath within the block in order to get a reference that stays valid until after the block finishes.
The UITableView's method to retrieve the cell is
- (UITableViewCell *)cellForRowAtIndexPath:(NSIndexPath *)indexPath
If the return value (a cell) is nil the cell at index path indexPath is not visible.
So, the completion block will look as follows:
if (error == nil && ... ) {
BarCell* cell = [self.tableView cellForRowAtIndexPath:indexPath]
if (cell) {
cell.imageViewImagemBar.image = [UIImage imageWithData:data];
[cell.activityFotoBar stopAnimating];
}
}
Which also safely handles the case where the cell is nil.
Note: while this "works" it's still a naïve approach. A more sophisticated approach would cache the (decoded) images and possibly has some "look ahead and eager eviction strategy" for better user experience and lower memory foot-print.
Note that imageWithData: may still be costly since it may require to decode, decompress and resize the image before it can be rendered. Basically, this can be performed beforehand, with an offscreen context.
I use the "famous" boilerplate code that almost everybody uses for handling cellForRowAtIndexPath:
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"someCustomCellID"];
if (cell == nil) // nothing to recycle from the queue: create a new cell
but this give me lots of problems 'cause my cells contains images that I load async and the two functionalities (dequeueing and async load) often conflict. So I try creating every time a new cell and it works pretty well and fast.
But I have a doubt: should I still call dequeueReusableCellWithIdentifier to free the memory even if I ignore the returned value and create the cells each time?
I suppose the cells not used anymore are automatically deallocated (as they should be), but I wonder if the caching queue may require the explicit "free" with the dequeue call...
dequeueReusableCellWithIdentifier is not designed to prevent memory leaks but it is designed for performance (one way is by reducing memory usage). When using the dequeue method, scrolling a table view will be much smoother when there are several rows of data. I would recommend you get your async loading to work with the dequeue method, especially if you see any lag while scrolling. If you would like an example of how to do this see Apple's LaxyTableImages Example. If you do, however, determine that you do not want to reuse cells, then simply pass nil as the reuseIdentifier when creating you cells.
dequeueReusableCellWithIdentifier helps keep down the number of times you load an reload things from files or XIB. If you have a custom cell or cells with non-standard contents, it can be a lot faster to re-use a cell that already has it all setup.
I recall working on something similar, but in our case, we loaded images into Core Data objects asynchronously and had the cell observe the image in that object. When the image was loaded, the cell was notified and it updated its image view.
When the cell came back out to us via dequeueReusableCellWithIdentifier, we stopped it from observing and set it to observe the next object's image.
Just use prepareForReuse: in your UITableViewCell's class, to stop the async load, before the cell is used again.
Yes, you need to use dequeueReusableCellWithIdentifier to avoid leaks, if you are dynamically allocating cells.
Presumably your asynchronous image loading works something like this:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell"];
if (!cell) {
...
}
NSURLRequest *request = [self URLRequestForIndexPath:indexPath];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
cell.imageView.image = [UIImage imageWithData:data];
}];
return cell;
}
That code has a problem. By the time the completion handler runs, the table view may have reused the cell for a different row!
We need to change the completion block so it looks up the cell for the row when it's ready to set the image:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell"];
if (!cell) {
...
}
NSURLRequest *request = [self URLRequestForIndexPath:indexPath];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
if (cell)
cell.imageView.image = [UIImage imageWithData:data];
}];
return cell;
}
You should use the method, always. If it returns a cell, use that cell. If it returns nil, then and only then should you create a cell.
If the method is demanding a cell for an image which you have not yet loaded, then you should provide an alternative. Perhaps a UIImageView with a customized, animated progress spinner? Then, when your image arrives, you can use the proper image when necessary.
Do not worry about deallocating cells. Once you give them to the table view, they are the table view's responsibility (at least as far as memory management is concerned).