UISegmentedControl with two UICollectionViews to display images. MULTITHREADING - ios

So my goal is to create something like this:
My problem is when I switch between segments, I would like to show a UIActivityViewIndicator spinning, as it takes some time to load the images (stored locally so in theory this should be much faster...). But the thread seems to be getting back before switching to the new cells' images.
What am I doing wrong?
Here is my code:
- (void)reloadCollection {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[_myCollection reloadData];
[_myCollection layoutIfNeeded];
dispatch_async(dispatch_get_main_queue(), ^{
_spinnerView.hidden = YES;
});
});
}
- (IBAction)segmentedControlSwitched:(id)sender{
_spinnerView.hidden = NO;
[self reloadCollection];
}
-(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
return 1;
}
-(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
if ([_mySegmentedControl selectedSegmentIndex] == 0){
return [_photosTaken count];
}else
return [_photosTagged count];
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
UICollectionViewCell *cell;
UIImageView *imageView;
if ([_mySegmentedControl selectedSegmentIndex] == 0){
cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"cell" forIndexPath:indexPath];
imageView = [[UIImageView alloc] initWithImage:[_photosTaken objectAtIndex:indexPath.row]];
}
else
{
cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"cell1" forIndexPath:indexPath];
imageView = [[UIImageView alloc] initWithImage:[_photosTagged objectAtIndex:indexPath.row]];
}
imageView.frame = cell.frame;
[cell setBackgroundView:imageView];
return cell;
}
Thanks,
João Garcia.

You can't call UIKit operations on a background thread, unless the documentation explicitly states that you can.
In any case, your problem is not one that should be solved simply by throwing things on a background thread.
You say yourself, for some reason the loading thread is taking too long. Why not use the time profiler in instruments to find out why?
You'll probably find that the issue is that you are creating a brand new image view every time and adding it as the cells background view.
Creating, adding and removing views is an expensive operation that should not be performed while scrolling. Your custom collection view cells should already have an image view, which you've added in the storyboard or a custom init method, and created an outlet or property for. It's very unusual not to use a subclassed cell, since the base cell has nothing in the content view.
When populating the cell, just set the image view's image property to your stored image. This should be fast enough that you don't need a spinner.
From your comments, it sounds like you also have these problems:
you are using images that are far too large. For display in a list you must use appropriately sized thumbnail images
you are performance testing on the simulator. This is irrelevant to real-life app performance on a device. The memory situation is completely different and the speed of things can be faster or slower depending on the operation (usually, CPU-based is faster, but GPU-based is slower).

dispatch_get_global_queue get a concurrent dispatch queue in which multiple tasks are run parallel. It is a background queue, UI updating should be performed on mainQueue, so [_myCollection layoutIfNeeded] should be moved into dispatch_async(dispatch_get_main_queue(),
updating UI on concurrent background queue will cause weird behaviour such as pausing or freezing.
And you can try this.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
// do time consuming things here
dispatch_sync(dispatch_get_main_queue(), ^{
[self.collectionView reloadData];
});
dispatch_sync(dispatch_get_main_queue(), ^{
[self.activityView stopAnimating]; // after collectionView is updated
});
});

Instead of dispatch_async, try using dispatch_sync for _spinnerView.hidden = YES; on the main queue.

It seems that the issue here was the size of the photos as #jrturton mentions in his answer.
I shortened the size (to about 10s of KBs) and it works smoothly.

Related

UICollectionView cell draws top left

This is my first time working with UICollectionView.
I've got everything set up and laid out as it should be (as far as I know). I've had a little bit of difficulty with the way the dequeue function works for the UICollectionView, but I think I've gotten past that. It was tricky setting up my custom cell classes when I didn't know if initWithFrame would be called or prepareForReuse.
I'm pretty sure the problem lies within the prepareForReuse function, but where is the question.
What happens is, the cells will apparently randomly draw at the top-left of the collection view and some cells will not be where they belong in the grid. (see image attached)
When bouncing, scrolling, and zooming (so as to cause reuse to occur), the problem happens. Randomly a slide will appear in the top left, and other slides will randomly disappear from the grid.
( I need more rep to post an image. Ugh. :| If you can help me, I'll email you the image. bmantzey#mac.com )
-(UICollectionViewCell*)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
Slide* thisSlide = [_presentation.slidesInEffect objectAtIndex:indexPath.row];
[BuilderSlide prepareWithSlide:thisSlide];
BuilderSlide* cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"PlainSlide" forIndexPath:indexPath];
return cell;}
I'm using a static method to set the Slide object, which contains the data necessary to either prepare the asynchronous download or retrieve the image from disk cache.
It's simply:
+(void)prepareWithSlide:(Slide*)slide{
if(s_slide)
[s_slide release];
s_slide = [slide retain];}
I'm not sure if it's a big no-no to do this but in my custom Cell class, I'm calling prepareForReuse in the initWithFrame block because I need that setup code to be the same:
-(id)initWithFrame:(CGRect)frame{
self = [super initWithFrame:frame];
if(self)
{
[self prepareForReuse];
}
return self;}
Here's the prepareForReuse function:
-(void)prepareForReuse{
CGSize size = [SpringboardLayout currentSlideSize];
[self setFrame:CGRectMake(0, 0, size.width, size.height)];
self.size = size;
// First remove any previous view, so as not to stack them.
if(_builderSlideView)
{
if(_builderSlideView.slide.slideID == s_slide.slideID)
return;
[_builderSlideView release];
}
for(UIView* aView in self.contentView.subviews)
{
if([aView isKindOfClass:[BuilderSlideView class]])
{
[aView removeFromSuperview];
break;
}
}
// Then setup the new view.
_builderSlideView = [[BuilderSlideView alloc] initWithSlide:s_slide];
self.builderCellView = _builderSlideView;
[s_slide release];
s_slide = nil;
[self.contentView addSubview:_builderSlideView];
if([SlideCache isImageCached:_builderSlideView.slide.slideID forPresentation:_builderSlideView.slide.presentationID asThumbnail:YES])
{
[_builderSlideView loadImageFromCache];
}
else
{
[_builderSlideView loadView];
}}
Finally, when the slide image has been downloaded, a Notification is posted (I plan on changing this to a delegate call). The notification simply reloads the cell that has received an update. Here's the notification code:
-(void)didLoadBuilderCellView:(NSNotification*)note{
BuilderCellView* cellView = [[note userInfo] objectForKey:#"cell"];
BuilderSlideView* slideView = (BuilderSlideView*)cellView;
NSIndexPath* indexPath = [self indexPathForSlide:slideView.slide];
if(indexPath)
[self.collectionView reloadItemsAtIndexPaths:[NSArray arrayWithObject:indexPath]];}
Note that the slide objects exist in the model.
Any ideas as to what may be causing this problem? Thanks in advance!
The problem that was causing the cells to draw top left and/or disappear was caused by infinite recursion on a background thread. Plain and simply, I wasn't implementing the lazy loading correctly and safely. To solve this problem, I went back to the drawing board and tried again. Proper implementation of a lazy loading algorithm did the trick.

UITableView thumbnail image performance

My image data is being loaded from core data into a UITableView cell. These images are automatically scaled by the OS (as far as I know) in cellForRowAtIndexPath:indexPath:. Not surprisingly, this causes a lot of lag while scrolling through the table view.
Similarly, I have a UICollectionViewController that loads all the same images into a collection view similar to the iOS Photos app and again, the images are scaled in cellForItemAtIndexPath:indexPath. Scrolling is laggy, but it also takes a very long time for the VC to load.
What is the best way to optimize performance in this scenario?
I've done some research and have come up with a couple possible solutions:
Initialize a "thumbnailArray" in viewDidLoad:animated:, scaling all the images I need before the table/collection view is loaded, then use this new array as the data source for the views. I figure this will solve the scrolling issue, but not the collection view loading issue.
Create new properties for thumbnail data in my image wrapper class. This data would be created when the image wrapper object is created (i.e. when the user adds an image) and saved in core data. I think this would be preferred over option #1.
Is option two the best way to go, or is there a better solution I am unaware of?
Here are my cellForRowAtIndexPath:indexPath and cellForItemAtIndexPath:indexPath: methods:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
CMAEntryTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"entriesCell" forIndexPath:indexPath];
CMAEntry *entry = [self.entries objectAtIndex:indexPath.item];
cell.speciesLabel.text = [entry.fishSpecies name];
cell.dateLabel.text = [self.dateFormatter stringFromDate:entry.date];
if (entry.location)
cell.locationLabel.text = [entry locationAsString];
else
cell.locationLabel.text = #"No Location";
if ([entry.images count] > 0)
cell.thumbImage.image = [[entry.images objectAtIndex:0] dataAsUIImage];
else
cell.thumbImage.image = [UIImage imageNamed:#"no_image.png"];
if (indexPath.item % 2 == 0)
[cell setBackgroundColor:CELL_COLOR_DARK];
else
[cell setBackgroundColor:CELL_COLOR_LIGHT];
return cell;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"thumbnailCell" forIndexPath:indexPath];
UIImageView *imageView = (UIImageView *)[cell viewWithTag:100];
[imageView setImage:[[self.imagesArray objectAtIndex:indexPath.item] dataAsUIImage]];
return cell;
}
Thanks in advance!
Cohen
Thanks for your input, guys, but this is the solution that worked really well for me. The only thing I don't like is that now I store an NSData instance of the full image, and two different sized thumbnail images in core data; however, that doesn't seem to be a problem.
What I did was add a couple attributes to my NSManagedObject subclass to store the thumbnail images. The thumbnail data is initialized when the image is selected by the user, then saved in core data along with the original image.
Then, I load the thumbnails into a collection asynchronously in the view controllers I need them.
Works great. Gets rid of all issues I was experiencing.
1 I think the best approach is to load images in a background thread so the tableview loads quickly.
2 Also you can use coredata feature of batchsize to load only necessary data.
3 Perhaps using some type of cache in memory for the images may help
In one of my project i faced same kind of issue and i solved it by using AsyncImageView for loading the thumbimage for my tableview cell.It will load the image asynchronously.
#property (nonatomic,strong)AsyncImageView * thumbImageView;
self.thumbImageView =[[AsyncImageView alloc] init];
cell.thumbImageView.imageURL =[NSURL fileURLWithPath:UrlString];

tableView cell image which is asynchronous loaded flickers as you scroll fast [duplicate]

This question already has answers here:
Async image loading from url inside a UITableView cell - image changes to wrong image while scrolling
(13 answers)
Closed 9 years ago.
Im using a asynchronous block (Grand central dispatch) to load my cell images. However if you scroll fast they still appear but very fast until it has loaded the correct one. Im sure this is a common problem but I can not seem to find a away around it.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
// Load the image with an GCD block executed in another thread
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:[[[appDelegate offersFeeds] objectAtIndex:indexPath.row] imageurl]]];
dispatch_async(dispatch_get_main_queue(), ^{
UIImage *offersImage = [UIImage imageWithData:data];
cell.imageView.image = offersImage;
});
});
cell.textLabel.text = [[[appDelegate offersFeeds] objectAtIndex:indexPath.row] title];
cell.detailTextLabel.text = [[[appDelegate offersFeeds] objectAtIndex:indexPath.row] subtitle];
return cell;
}
At the very least, you probably want to remove the image from the cell (in case it is a re-used cell) before your dispatch_async:
cell.imageView.image = [UIImage imageNamed:#"placeholder.png"];
Or
cell.imageView.image = nil;
You also want to make sure that the cell in question is still on screen before updating (by using the UITableView method, cellForRowAtIndexPath: which returns nil if the cell for that row is no longer visible, not to be confused with the UITableViewDataDelegate method tableView:cellForRowAtIndexPath:), e.g.:
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
cell.imageView.image = [UIImage imageNamed:#"placeholder.png"];
// Load the image with an GCD block executed in another thread
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:[[[appDelegate offersFeeds] objectAtIndex:indexPath.row] imageurl]]];
if (data) {
UIImage *offersImage = [UIImage imageWithData:data];
if (offersImage) {
dispatch_async(dispatch_get_main_queue(), ^{
UITableViewCell *updateCell = [tableView cellForRowAtIndexPath:indexPath];
if (updateCell) {
updateCell.imageView.image = offersImage;
}
});
}
}
});
cell.textLabel.text = [[[appDelegate offersFeeds] objectAtIndex:indexPath.row] title];
cell.detailTextLabel.text = [[[appDelegate offersFeeds] objectAtIndex:indexPath.row] subtitle];
return cell;
Frankly, you should also be using a cache to avoid re-retrieving images unnecessarily (e.g. you scroll down a bit and scroll back up, you don't want to issue network requests for those prior cells' images). Even better, you should use one of the UIImageView categories out there (such as the one included in SDWebImage or AFNetworking). That achieves the asynchronous image loading, but also gracefully handles cacheing (don't reretrieve an image you just retrieved a few seconds ago), cancelation of images that haven't happened yet (e.g. if user quickly scrolls to row 100, you probably don't want to wait for the first 99 to retrieve before showing the user the image for the 100th row).
This is a problem with async image loading...
Let's say you have 5 visible rows at any given time.
If you are scrolling fast, and you scroll down for instance 10 rows, the tableView:cellForRowAtIndexPath will be called 10 times. The thing is that these calls are faster than the images are returned, and you have the 10 pending images from different URL-s.
When the images finally come back from the server, they will be returned on those cells that you put in the async loader. Since you are reusing only 5 cells, some of these images will be displayed twice on each cell, as they are downloaded from the server, and that is why you see flickering. Also, remember to call
cell.imageView.image = nil
before calling the async downloading method, as the previous image from the reused cell will remain and also cause a flickering effect when the new image is assigned.
The way around this is to store the latest URL of the image you have to display on each cell, and then when the image comes back from the server check that URL with the one you have in your request. If it is not the same, cache that image for later.
For caching requests, check out NSURLRequest and NSURLConnection classes.
I strongly suggest that you use AFNetworking for any server communication though.
Good luck!
The reason of your flicker is that your start the download for several images during the scrolling, every time you a cell is displayed on screen a new request is performed and it's possible that the old requests are not completed, every tile a request completes the image is set on the cell, so it's if you scroll fast you use let's say a cell 3 times = 3 requests that will be fired = 3 images will be set on that cell = flicker.
I had the same issue and here is my approach:
Create a custom cell with all the required views. Each cells has it's own download operation. In the cell's -prepareForReuse method. I would make the image nil and cancel the request.
In this way for each cell I have only one request operation = one image = no flicker.
Even using AFNetworking you can have the same issue if you won't cancel the image download.
The issue is that you download the image on the main thread. Dispatch a background queue for that:
dispatch_queue_t myQueue = dispatch_queue_create("myQueue", NULL);
// execute a task on that queue asynchronously
dispatch_async(myQueue, ^{
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:[[[appDelegate offersFeeds] objectAtIndex:indexPath.row] imageurl]]];
//UI updates should remain on the main thread
UIImage *offersImage = [UIImage imageWithData:data];
dispatch_async(dispatch_get_main_queue(), ^{
UIImage *offersImage = [UIImage imageWithData:data];
cell.imageView.image = offersImage;
});
});

UICollectionview Scrolling choppy when loading cells

I have a gallery in my app utilizing a UICollectionView. The cells are approximately 70,70 size. I am using ALAssets from the ALAssetLibrary in the gallery which I have stored in a list.
I am using the usual pattern for populating the cells:
-(UICollectionViewCell*)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
mycell = [collectionView dequeueReusableCellWithReuseIdentifier:cellIdentifier forIndexPath:indexPath];
mycell.imageView.image = [[UIImage imageWithCGImage:[alassetList objectAtIndex:indexpath.row] thumbnail]];
return mycell;
}
My gallery is scrolling choppy. I don't understand why this is. I've tried adding a NSCache to cache the thumbnail images (thinking maybe creating the images was expensive) but this did not help for the performance.
I would expect the UI to be as buttery as the stock app.
I am now suspecting it may be something in the UICollectionViewCell prepareForReuse that may be holding up the dequeueReusableCellWithReuseIdentifier method but using instruments I was not able to find this.
Any other thing that may be be causing this? Is there a "faster" way to prepare the UICollectionViewCell or to dequeue them in a faster fashion?
So anybody having scrolling issues should do this
add these 2 lines after your dequeue
cell.layer.shouldRasterize = YES;
cell.layer.rasterizationScale = [UIScreen mainScreen].scale;
I would assume the "choppyness" is coming from the UIImage allocation, not anything with the dequeueReusableCellWithReuseIdentifier method. I'd try doing the image allocation on a background thread and see if that makes things a little more buttery.
-(UICollectionViewCell*)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
mycell = [collectionView dequeueReusableCellWithReuseIdentifier:cellIdentifier forIndexPath:indexPath];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^(void) {
// Load image on a non-ui-blocking thread
UIImage *image = [[UIImage imageWithCGImage:[alassetList objectAtIndex:indexpath.row] thumbnail]];
dispatch_sync(dispatch_get_main_queue(), ^(void) {
// Assign image back on the main thread
mycell.imageView.image = image;
});
});
return mycell;
}
More details can be found here: https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/Multithreading/Introduction/Introduction.html
Load your images using NSURLConnection's sendAsynchronousRequest:queue:completionHandler:
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:urlString]];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
[cell.imageView setImage:[UIImage imageWithData:data]];
}];
Tick on clip subviews and opaque in attributes inspector and tick off paging enabled.
If anyone is using PhImageManager, turning off synchronous solved the problem. (deliveryMode = .FastFormat can also give additional performance enhancement, but the tradeoff is the thumbnail will be of lower quality)
let option = PHImageRequestOptions()
option.deliveryMode = .Opportunistic
option.synchronous = false
PHImageManager().requestImageForAsset(phAsset, targetSize: CGSizeMake(2048, 2048), contentMode: .AspectFit, options: option, resultHandler: { (image, objects) in
self.imageView.image = image!
})
I had issues with UICollectionView scrolling.
What worked (almost) like a charm for me: I populated the cells with png thumbnails 90x90. I say almost because the first complete scroll is not so smooth, but never crashed anymore...
In my case, the cell size is 90x90.
I had many original png sizes before, and it was very choppy when png original size was greater than ~1000x1000 (many crashes on first scroll).
So I select 90x90 (or the like) on the UICollectionView and display the original pngs (no matter the size). Hope this helps others.

Wrong data populated in tableview cell on dispatch queue

I have an iOS app that has a UITableView with custom TableViewCells that contain a UIImageView. The image is loaded from a web service, so during the initial load, I display a "loading" image, and then use gcd to dispatch and get the image matching the data for that cell.
When I use a DISPATCH_QUEUE_PRIORITY_HIGH global queue to perform the image fetch, I sporadically get the wrong images loading in the tableview cells. If I use my own custom queue then the correct images get populated into the cells but the tableview performance is awful.
Here is the code...
// See if the icon is in the cache
if([self.photoCache objectForKey:[sample valueForKey:#"api_id"]]){
[[cell sampleIcon]setImage:[self.photoCache objectForKey:[sample valueForKey:#"api_id"]]];
}
else {
NSLog(#"Cache miss");
[cell.sampleIcon setImage:nil];
dispatch_queue_t cacheMissQueue = dispatch_queue_create("cacheMissQueue", NULL);
//dispatch_queue_t cacheMissQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(cacheMissQueue, ^{
if(sample.thumbnailFilename && sample.api_id){
NSData *thumbNailData = [[NSData alloc] initWithContentsOfFile:sample.thumbnailFilename];
UIImage *thumbNailImage = [[UIImage alloc]initWithData:thumbNailData];
if(thumbNailImage){
// Set the cell
dispatch_sync(dispatch_get_main_queue(), ^{
[[cell sampleIcon]setImage:thumbNailImage];
[cell setNeedsLayout];
});
// save it to cache for future references
NSLog(#"DEBUG: Saving to cache %# for sample %#",sample.thumbnailFilename,[sample objectID]);
[self.photoCache setObject:thumbNailImage forKey:sample.api_id];
}
}
});
dispatch_release(cacheMissQueue);
}
Watching the WWDC 2012 session #211 helped a lot and I changed the code from using GCD to NSOperationQueue and it solved the problem.
New code...
[[self imgQueue]addOperationWithBlock:^{
if(sample.thumbnailFilename && sample.api_id){
NSData *thumbNailData = [[NSData alloc] initWithContentsOfFile:sample.thumbnailFilename];
UIImage *thumbNailImage = [[UIImage alloc]initWithData:thumbNailData];
if(thumbNailImage){
// Set the cell
[[NSOperationQueue mainQueue]addOperationWithBlock:^{
[[cell sampleIcon]setImage:thumbNailImage];
[cell setNeedsLayout];
}];
// save it to cache for future references
[self.photoCache setObject:thumbNailImage forKey:sample.api_id];
}
}
}];
When you finally get an image, you need an association between the indexPath of the cell and the image. Since this is on a backgound thread, what I suggest you do is post a nofification using a block to the mainQueue that such and such an image is available. On the main thread only, you ask the tableView for the array of visible cells, and if the cell you have an image for is showing, you can then set the image directly at that time (your on the main thread, you know the cell is there and showing, and its not going to change for this runloop iteration.) If the cell is not showing, no problem, next time that cell comes into scope you will have the image waiting for it. I am doing this now in my app, its been out many months, and its all working well and getting good reviews on responsiveness (just as your app will if you do this!)

Resources