Lazy Loading of several images in UITableView - ios

What are the best practices to download several images and load each of them in a UIImageView which is inside a UITableViewCell? Particularly, should I resize/replace the UIImageview after downloading or should I resize the image to fit into the UIImageView. Note that resizing/replacing UIImageView also resize/replace UITableViewCell. Will it cause any issue?

A couple of thoughts:
What are the best practices to download several images and load each of them in a UIImageView which is inside a UITableViewCell?
The best practices regarding the downloading of images include:
Definitely avail yourself of lazy loading (load images as you need them and not before).
Download images asynchronously.
Make sure your download technique will cancel requests for tableview cells that are no longer visible. For example, if you're on a slow network and scroll down quickly on your table view, you don't want to tie up your device downloading images that aren't visible anymore.
Make sure you don't make too many concurrent requests of your server. In iOS, when you exceed 5 or 6 concurrent requests, subsequent requests will freeze until the prior ones complete. In worst case scenarios, the subsequent requests will actually start failing as they timeout.
Cache your results. At the very least, cache them in memory. You might also want to cache them to persistent storage (a.k.a. "disk"), too.
If you were going to write your own code for the asynchronous operations, caching, etc. you might want to use NSOperationQueue instead of GCD so that I could constrain number of background requests and make the requests cancelable. You would use NSCache to cache the images. And you'd probably use a UITableViewCell subclass (or a category) so that you can save weak reference to "previous" operation, so that you can cancel any incomplete, prior requests.
As you can see, this is non-trivial, and I'd suggest you using an existing UIImageView category, such as those available as part of SDWebImage or AFNetworking. IMHO, the former is a little richer (e.g. offers disk caching), but if you're doing a lot of networking and want to stick with a single framework, AFNetworking does a great job, too.
Later you ask:
Particularly, should I resize/replace the UIImageview after downloading or should I resize the image to fit into the UIImageView. Note that resizing/replacing UIImageView also resize/replace UITableViewCell. Will it cause any issue?
If your images are larger than what your cell's thumbnail view requires, you have two approaches. First, you can use a contentMode of UIViewContentModeScaleAspectFit or UIViewContentModeScaleAspectFill (and if you use AspectFill, make sure you also set clipsToBounds to YES). Even better, you can actually resize the image after you download it.
It's personal opinion, but I think it's a better UX to have a UIImageView of fixed size on the cell and then when the asynchronous image download is done, just set the image property of the UIImageView. You want the images to gracefully appear in your UI as they're downloaded, but you generally don't want a jarring re-layout of the view while the user is already in the process of reading what's there. If your design absolutely necessitates the re-layout of the cell, then you can just call reloadRowsAtIndexPaths.

A common practice for lazy-loading images in a UITableViewCell is to use a notification callback to let the UITableViewCell know when the image has been received.
In essence, you'll want to create a subclass of UIImageView that has an imageURL field that, when changed, fires off a request for the image, and use that instead of a standard UIImageView:
Interface for the UIImageView subclass:
#property (nonatomic, copy) NSString *imageURL;
Implementation for the UIImageView subclass:
//synthesize property
#synthesize imageURL = _imageURL;
- (void)setImageURL:(NSString *)imageURL {
if(_imageURL)
[[NSNotificationCenter defaultCenter] removeObserver:self name:_imageURL object:nil];
_imageURL = [imageURL copy];
//if imageURL is valid...
if(_imageURL.length) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(didReceiveImage:) name:_imageURL object:nil];
//fire off some asynchronous image fetch
//when the image fetch completes, sent off a notification using the imageURL as the notification name
//It's up to you to create the implementation for this yourself
... [MyImageManager fetchImage:_imageURL notificationName:_imageURL];
}
}
- (void)didReceiveImage:(NSNotification*)notification
{
//handle your received image here
if([notification.object isKindOfClass:[UIImage class]])
{
self.myCustomImageView.image = notification.object;
}
}
And then in the UITableViewCell class when you override prepareForReuse:
- (void)prepareForReuse {
[super prepareForReuse];
self.myCustomImageView.imageURL = nil;
self.myCustomImageView.image = nil;
//do the rest of your prepareForReuse here:
...
}
Now, as far as the whole re-sizing of the images vs resizing of the imageview, what you ought to do is leave the imageView's size alone, and use the contentMode property to handle the varying sizes for the resulting images. Your possible values are:
UIViewContentModeScaleToFill,
UIViewContentModeScaleAspectFit,
UIViewContentModeScaleAspectFill,
UIViewContentModeRedraw,
UIViewContentModeCenter,
UIViewContentModeTop,
UIViewContentModeBottom,
UIViewContentModeLeft,
UIViewContentModeRight,
UIViewContentModeTopLeft,
UIViewContentModeTopRight,
UIViewContentModeBottomLeft,
UIViewContentModeBottomRight,
Each of which has their own respective result - you will probably want to use UIViewContentModeScaleAspectFit as this will re-size the image to fit the imageView, without distorting it - this may leave empty margins to the size or top/bottom of the imageView. Aspect fill will do a similar thing, only it will resize the image to be large enough to fill the entire imageView, and may cut off the sides or top/bottom of the image. Scale to fill will just stretch the image to fill the imageView.

Related

iOS - Should A Hidden UIImageView Of A UITableViewCell NOT Have An Assigned Image Until It's Visible?

I have UITableViewController where in its cellForRowAtIndexPath method, if a certain condition is true about the UITableViewCell, I make its UIImageView visible. In my storyboard, I already have the UIImageView with an assigned image and hidden by default.
My question is this. Is it better, in terms of limiting my app's memory usage, to have the UIImageView NOT assigned to an image in the storyboard and just call this code when it needs to be displayed? :
myCell.myImageView.image = [UIImage imageNamed:#"myImage.png"];
myCell.myImageView.hidden = NO;
I'm asking this because I'm wondering if the constant repetition of images will hog up too much memory even though they're hidden, and if it's better to ONLY assign an image when the UIImageView's hidden attribute is about to be set to NO (to make it visible).
imageNamed: caches the image and is dumped when your app reaches a memory warning.
If the same key (#"myImage.png") is used in multiple places it reads from the same memory space and does not create multiple instances of the bitmap.
Memory issues with images crop up mostly when you're holding onto lots of large images strongly. There's many strategies, like using SDWebImage and letting that handle your image cache.
As always, resize your images as best you can to the size of the device that's viewing it, implement sensible caches and don't worry about memory so much!
You can check if your memory is being eaten up with the memory chart under the "Debug Navigator" (command - 6) and just scroll to see how your memory is being used.

Best practice for preloading and caching images (AFNetworking)

I was wondering what is the best practice for this very common action:
Say i have a collection view, and each cell of this collection view has an imageView that receives a url for an image.
The images are large, so i want to preload all the images in advance (or say 5 in advance).
I'm using AFNetworking imageView Category.
So here's what i did - creating a temp UIImageView and set the url to it each iteration, but it seems not to be working right. (the images still takes time to show up, while i want it to be instantly, and if i close the internet connection, the images do not load from cache).
- (void)preloadGalleryImages {
for (GalleryItem *item in _galleryItems) {
UIImageView *tempImageView = [[UIImageView alloc] init];
[tempImageView setImageWithURL:item.imageURL placeholderImage:IMAGE(#"placegolder")];
}
}
}
Thanks,
There is a universal way of preheating images in collection views that was implemented in one of the Apple PhotosKit samples. You can anticipate user actions and fetch images that might soon appear on the display (that are close to the viewport). This feature is implemented in Nuke and DFImageManager.

Prepare UIImage with lots of layers on background thread

I have the problem that I have some really big UIScrollView and tons of images loaded on it as user scrolls. Images are stored on the device, however I receive information from server what to display on particular part of UIScrollView. When user scrolls a bit I need to show images at new position as I cannot afford to draw whole UIScrollView with images at startup. For the background I had one relatively small image which I move throughout the View. But the problem is that on top of that background I should draw a lot of UIImage objects(about 300-400) which are not particulary bih however are separeted on layers(one image on top of other on top of other etc.). Blocking scrolling while drawing is NOT an option.
Now I'm trying to decide which approach will suite my best:
Add all needed images to UIView on background thread and then just add UIView to ScrollView on main thread(which hopefully wont take long). Here when scroll somewhere I will need to calculate and create new UIView with objects and position it next to existing and eventualy to remove first UIView with all objects and layers when user continues to scroll in some direction.
Combine all layers in image with CoreGraphics and present them as objects with already decided layers. In this way I can remove specific object(image) from scroll view. When user scrolls I just create new objects and add them to view as full objects, and can remove objects when user scrolls enough instead of removing whole view. The problem here is adding multiple objects to UIScrollView on main thread, however when they are combined they won't be more than 15-20 objects.
My biggest concerns are performance and threading. As I said I cannot block main thread(or let's say cannot do this for a time that user will notice) and cannot combine images at my graphics department as they have tons of variatons which are decided at runtime. That's why I'm thinking of a way to prepare data on background thread and just really fast add it on main thread instead of preparing and adding it on main thread(which will block UI).
Every help will be greatly appriciated!
Regards,
hris.to
Look at using CATiledLayer for a UIView backing. It is was designed for this.
I have a map that has one UIView in a UIScrollView and the UIView is sized to the full size of the entire map. The CATiledLayer handles when to draw each tile of the view.
Ok, so I'm writing here just to let you know how I fix my issue.
Main problem was that I was moving a background picture while scrolling(so I don't load an enormous file) and while doing that I was fetching information from server and try to draw on the same tiles which makes system crash with well known crash:
CALayer was muted while enumerated
I was depending on performSelector method along with #synchronized but it turns out that this is not effective and sometimes two threads(main UI thread and background thread) were trying to change same tiles on screen. So basically what I did is to change background fetching and drawing from:
[self performSelectorOnBackgroundThread]
to using concrete background thread, to which I store reference:
#property(nonatomic, strong) NSThread* backgroundThread;
And now each time I need to load new tiles or I need to move background, I'm cancelling this thread(and make sure it's cancelled) before start any other operation. Also there was a little problem when switching between view with this thread and other views, as my thread hangs, and I needed to set a timeout:
[_backgroundThread cancel];
int currentAttempts = 0;
while( [_backgroundThread isExecuting] )
{
if( currentAttempts == MAX_ATTEMPTS_TO_CANCEL_THREAD )
{
//make sure we dont hang and force background
_backgroundThread = nil;
return;
}
[_backgroundThread cancel];
currentAttempts++;
}
In my 'scrollViewDidScroll' however, I didn't use the recursion as this results in slight UI blocks while scrolling on older devices(such as iPhone 4) which is unacceptable. So there I basically just cancel the thread and hope to get cancelled quick enough(which with dozens of tests appears to be working):
- (void)scrollViewDidScroll:(UIScrollView*)scrollView
{
[_backgroundThread cancel];
//move background around
[self moveBackground];
}
Downside of this approach is that you need lots of check in your background thread as calling 'cancel' to it won't actually cancel anything. As per apple docs it'll only change isCancelled state of your thread and you are responsible to make this thread quit in basically the same way as it'll quit normally(so the system has a chance to cleanup after your thread):
if( [_backgroundThread isCancelled] )
{
return;
}
Regards,
hris.to

setNeedsDisplay confusion

I'm using this to move my image:
meteorDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(moveImage:)];
Here is my moveImage: method:
-(void) moveImage:(CADisplayLink *)sender{
CGPoint img;
imgLocation = imgImageView.center;
if (img < 100) {
img.y++;
}
else{
imgLocation.y = 0;
}
imgImageView.center = imgLocation;
}
This updates the location of my image on screen no matter what. Why does setNeedsDisplay have no effect here? What is the point of setNeedsDisplay?
I assume imgImageView is an instance of UIImageView. If it's not an instance of UIImageView, edit your question to tell us what it is.
Changing the center property of a view never requires your app to redraw the view. The display server (which is a separate process named backboardd in iOS 6 and springboard in earlier versions) has a copy of your view's pixel data already. When you change a view's center, the view just informs the display server of its new position, and the display server copies the view's pixel data to the appropriate parts of the screen.
Thus changing the center property of a view never requires you to send setNeedsDisplay.
More specifically, UIImageView doesn't even implement drawRect:. Instead, UIImageView just sends its image's pixel data to the display server when you set its image. So even if you send setNeedsDisplay to a UIImageView, it still won't run any drawRect: method.
By the way, you can do this in your own UIView subclasses too, by setting self.layer.contents to a CGImageRef instead of implementing drawRect:. You will need to #import <QuartzCore/QuartzCore.h> to do this. Of course, UIImageView does other things too, like handling animated and resizable images.
When you set properties that affect the layout of an object, setNeedsDisplay will be called internally. You need to call it yourself for custom properties, etc, that don't automatically call it.
setNeedsDisplay only applies to the contents of a layer or view. I.e., if the contents of a view has changed and needs to be redrawn, you would use setNeedsDisplay to tell the view that it needs to be redrawn and that would trigger its drawRect method.
Transformations on a view, such as position, scale, and rotation, etc., are handled directly by the GPU and do not require the view to be redrawn; therefore, setNeedsDisplay does not apply when you're simply moving or transforming a view.

Why can't I update the contents of my UIScrollView using NSOperationQueue / NSOperationInvocation?

I think I remember something about UI being updated on the main thread, could be the explanation, however my issue is that I have a UIScrollView with 1000+ images in it, and I wrote a routine that quickly will toggle the images visible in the view to show their real image, and when out of visible view, they switch to a placeholder image. All the offscreen images share the same placeholder, greatly reducing memory load.
I wrote two methods that toggle the next few tiles of the view on/off, and are kicked off if you have scrolled enough in a certain direction. The scrolling is detected via the UIScrollViewDelegate callback method scrollViewDidScroll.
This strategy works fine, but the scrollview scrolling is a little jittery on slow devices (iPad 1). I wanted to increase performance, so I thought I could change the code so that the two methods that update the scrollview contents were NSOperationInvocation, added to an NSOperationQueue.
However, when I do this, even with priority set to VeryHigh, the routines to not update the UI even though they seem to be called, and the queue does grow, when scrolling. The observed behavior is that the images retain their placeholder images and do not switch on.
Why doesn't this work?
Code for the NSOperationInvocation:
ImageThumbView *thumb = [boardThumbsArray objectAtIndex:lowestLoadedThumbTag - i];
[loadedThumbsArray addObject:thumb];
[thumb showThumbImage];
ImageThumbView is a UIButton Subclass
- (void)showThumbImage {
if (thumbImagePath != nil) {
thumbImage = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL fileURLWithPath:thumbImagePath]]];
} else {
NSLog(#"%#", #"Cannot show thumb image: thumbImagePath is nil");
}
[self setBackgroundImage:thumbImage forState:UIControlStateNormal];
[self setNeedsDisplay];
}
Look at Apple's "PhotoScroller" sample code. You should not be having 1000 views as subviews of scrollview. Recycle the ones that are no longer visible.
Also look at WWDC 2010 "Designing apps with scrollview" video. That explains the photoscroller sample code
Thanks

Resources