CATiledLayer - how to use when async downloading images - iphone

In my application, I have got a scrollview, a bigger one with lots of subviews on it, subviews gets added dynamically based on scroll. I try to add subviews (imageviews) dynamically on scroll and when user scrolls then I try to fetch more images asynchronously based on pageSize etc from server and these images gets placed into it's corresponding imageview..
I have gone through Apple's WWDC Sessoion 104 as well which appears to be good for offline images.
I also try to resize the images on my scrollview in a ratio, which is fine I believe. The problem is When the number of images increases on the scrollview then application runs out of memory. It must be due to me using the images directly in an imageview instead of using CATiledLayer. But, I am looking for help in displaying async images on scrollview using CATiledLayer.
Many Thanks,
Reno Jones

I edit completely the answer based on the comments.
You should set a controller as delegate of the scrollview than in the scrollViewDidScroll: (or in scrollViewDidEndDecelerating: if you want to check it less often) method you can check if some your images are outside the visible part of the content view and then release (i.e. set the pointer to nil) them to save some memory.
For your convenience here's an example how to get the visible part of your content:
CGRect visibleRect;
visibleRect.origin = scrollView.contentOffset;
visibleRect.size = scrollView.bounds.size;
everything completely outside this rect should be released (of course you then have to reload them if user scroll: it may be tricky constantly load/unload...)

This in simple words is all I did.. I have already been downloading images async and caching them up on device, then I have added an UIImage (imageObject) property to my current tile class, which holds the image object being downloaded and then implemented the following and did the trick for me. And believe me or not, imageview loading vs CATiledLayer - CATiledLayer is 100x faster and efficient.
+ layerClass
{
return [CATiledLayer class];
}
- (void)drawRect:(CGRect)rect
{
[imgObject drawAtPoint:CGPointMake(0,0)];
}
Hope it helps someone else in future. Let me know if I can be of more help.

Related

How do I “redraw layers” in Core Graphics?

I remember that when I was reading the Apple documentation, it would mention that when you call a function such as addSubview, you are adding a “layer of paint,” so to speak, and every time it is called, another layer is overlaid.
This should be an easy question to answer, but I had a hard time thinking of keywords to google for, so please excuse the asking of such a simple question.
How do I clear the “layers” of a custom UIView?
My situation, as it may be relevant: I have these “user cards” that are displayed on the screen. They are initialized with some user images. The cards stay the same, but I call a method in my custom UIView (the card UIView) to redraw the images when I want to display a different user. The problem is that some elements of this custom UIView are transparent, and redrawing these images each time builds on that transparency (an obvious problem).
In Core Graphics, what you draw is what gets shown. The painter’s analogy only refers to a single frame. So if you’re using drawRect, you just don’t cache the previous drawing.
But I suspect you’re talking about some UIKit stuff where you’ve added subviews or sublayers. This will remove those leftover views if you just want to clear everything:
for (UIView *view in customView) {
[view removeFromSuperview];
}
for (CALayer *layer in customView.layer) {
[layer removeFromSuperlayer];
}
Ryan's code for clearing UIViews is more or less correct, but if you came here from Google looking for how to clear CLLayers from a view, I was getting a crash when I attempted to fast enumerate customView.layer like Ryan has in his example.
When I switched it to regular enumeration, it worked for me. Here's a code snippet:
for (int i = 0; i > yourView.layer.sublayers.count; i++) {
CALayer *layer = [self.yourView.layer.sublayers objectAtIndex:i];
[layer removeFromSuperlayer];
}

How to zoom two Images placed side by side, together?

I have two scroll-views placed side by side and they can be zoomed individually. I've done this by putting my view inside a scrollview and setting the zoom-scale for the scroll-views. So far, it works fine! Now, there's a new requirement to zoom the two images together so that if I zoom one image, the other is zoomed automatically with the same zoom scale. I was given the roambi app as reference in which two scrollviews can be scrolled together by scrolling either one of them for convenience during comparison. Basically, what I'm doing is also comparison between the two views. I've gone through scrollview delegate methods but was unable to achieve the required results. How do I do this?
I've never done this, but off the top of my head I would say first you need to get the zooming being the same in both of them (as per above), then you'll have to use the delegate methods to make sure both of your scrollviews have the same contentOffset value. i.e. when one changes via manual scrolling or via programmatic scrolling, you have to (using the delegate callbacks) set the other one to the same contentOffset value.
EDIT: As per request, adding a bit of (UNTESTED) code:
- (void)scrollViewDidScroll:(UIScrollView*)scrollView
{
if(scrollView == self.myFirstScroller)
{
self.mySecondScroller.contentOffset = self.myFirstScroller.contentOffset;
}
else {
self.myFirstScroller.contentOffset = self.mySecondScroller.contentOffset;
}
}
and zooming done similarly to above.
But if you're looking for some copy-paste solution you can just drop into your project, I'm afraid you'll have to teach yourself a bit more about scroll views. You should read the Apple Programming Guide, because scrollviews can be a bit tricky, and you often have to use quite a few of the delegate methods to get things working correctly.
I had implemented something similar a while ago (I did it for buttons). This is how I did it:
Take two UIScrollViews and reference them (I've used firstScrollView and secondScrollView)
Take two UIButtons and reference them (I've used firstImgBtn and secondImgBtn). Set the delegates to both the scrollviews and use the following delegate methods:
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView{
//return the respective button in the scrollview to be zoomed
if(scrollView==firstScrollView){
return firstImgBtn;
}
else{
return secondImgBtn;
}
}
- (void)scrollViewDidZoom:(UIScrollView *)scrollView{
// zoom in the other scrollview when one has zoomed
if(zoomTogether){//a bool to decide whether to zoom the two together or not
if(scrollView==firstScrollView){
secondScrollView.zoomScale = firstScrollView.zoomScale;
}
else{
firstScrollView.zoomScale = secondScrollView.zoomScale;
}
}
}
This can be applied to any subclass of UIView-in your case it will be UIImageViews

How to populate an image in UITableViewCell even if user scroll the table

I'll try to explain my self, I have ContactsViewController that shows a table view with a list of contacts (the model is an array of Contact objects), each cell display an image of a contact.
Currently what I do to populate the cell's UIImageView is this:
1. I override the Contact image property getter -
- (UIImage *)contactImage
{
if (!_contactImage) {
_contactImage = [[UIImage imageNamed:#"placeHolder.png"] retain];
[self asyncDownloadContactImageFromServer];
}
return _contactImage;
}
Then when I finish downloading the image I set it to the contactImage property and I post a ContactUpdatedImageNotification.
My ContactsViewController then get this notification and reload the cell of this contact, this will set the downloaded image to the cell's imageView.
The result of this is good async fetching of the images without blocking the UI while the user scroll the table view.
BUT there is something small that bothers me, when the a user scroll the table view and reveal new cells the new cell's image get download as expected but the cell's imageView is not updated with the new downloaded image till the user pick up his finger.
I supposed that I need to do something in another thread to make this effect, but I don't know how?
The image is not updated until the user stops scrolling due the code being executed in the default runloop, which gets delayed until scrolling finishes. This other question deals with the difference between the runloops, NSDefaultRunLoopMode vs NSRunLoopCommonModes and it precisely recommends not updating the images while scrolling since that can introduce jerkiness in the scrolling itself if you are not careful.
Also, now that you know about the existence of these runloop modes you will be able to find much more information about them in the xcode documentation or internet.
Hey Eyal visit following url...you will get answer and as well sample code...
tableview with different cell with different images
Hope this will help you...

Scrolling performance and UIImage drawing

I'm building a UITableView similar to iPod.app's album browsing view:
I'm importing all the artists and album artworks from the iPod library on first launch. Saving everything to CoreData and getting it back into an NSFetchedResultsController. I'm reusing cell identifiers and in my cellForRowAtIndexPath: method I have this code:
Artist *artist = [fetchedResultsController objectAtIndexPath:indexPath];
NSString *identifier = #"bigCell";
SWArtistViewCell *cell = (SWArtistViewCell*)[tableView dequeueReusableCellWithIdentifier:identifier];
if (cell == nil)
cell = [[[SWArtistViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier] autorelease];
cell.artistName = artist.artist_name;
cell.artworkImage = [UIImage imageWithData:artist.image];
[cell setNeedsDisplay];
return cell;
My SWArtistViewCell cell implements the drawRect: method to draw both the string and image:
[artworkImage drawInRect:CGRectMake(0,1,44,44)]
[artistName drawAtPoint:CGPointMake(54, 13) forWidth:200 withFont:[UIFont boldSystemFontOfSize:20] lineBreakMode:UILineBreakModeClip];
Scrolling is still choppy and I just can't figure out why. Apps like iPod and Twitter have butter smooth scrolling and yet they both draw some small image in the cell as I do.
All my views are opaque. What am I missing?
EDIT: here's what Shark says:
I'm not familiar with Shark. Any pointer as of what are these symbols related to? When I look at the trace of these, they all point to my drawRect: method, specifically the UIImage drawing.
Would it point to something else if the chokehold was the file reading? Is it definitely the drawing?
EDIT: retaining the image
I've done as pothibo suggested and added an artworkImage method to my Artist class that retains the image created with imageWithData:
- (UIImage*)artworkImage {
if(artworkImage == nil)
artworkImage = [[UIImage imageWithData:self.image] retain];
return artworkImage;
}
So now I can directly set the retained image to my TableViewCell as follow:
cell.artworkImage = artist.artworkImage;
I also set my setNeedsDisplay inside the setArtworkImage: method of my tableViewCell class. Scrolling is still laggy and Shark shows exactly the same results.
Your profiling data strongly suggests that the bottleneck is in the unpacking of your PNG images. My guess is that 58.5 % of your presented CPU time is spent unpacking PNG data (i.e. if the memcpy call is also included in the loading). Probably even more of the time is spent there, but hard to say without more data. My suggestions:
As stated before, keep loaded images in UIImage, not in NSData. This way you won't have to PNG-unpack every time you display an image.
Put the loading of your images in a worker thread, to not affect the responsiveness of the main thread (as much). Creating a worker is real easy:
[self performSelectorOnMainThread:#selector(preloadThreadEntry:) withObject:nil waitUntilDone:NO];
Preload images far ahead, like 100 rows or more (like 70 in the direction you're scrolling, keep 30 in the opposite direction). If all your images need to be 88x88 pixels on retina, 100 images would require no more than two MB.
When you profile more the calls to stuff named "png", "gz", "inflate" and so forth might not go way down your list, but they will certainly not affect the feeling of the application in such a bad way.
Only if you still have performance problems after this, I would recommend you look into scaling, and for instance loading "...#2x.png" images for retina. Good luck!
[UIImage imageWithData:] doesn't cache.
This means that CoreGraphic uncompress and process your image every single time you pass in that dataSource method.
I would change your artist's object to hold on a UIImage instead of NSData. You can always flush the image on memoryWarning if you get them a lot.
Also, I would not recommend using setNeedsDisplay inside the dataSource call, I would use that from within your cell.
SetNeedsDisplay is not a direct call to drawRect:
It only tells the os to draw the UIVIew again at the end of the runloop. You can call setNeedsDisplay 100 times in the same runloop and the OS will only call your drawRect method once.
If the delay's happening in your -drawRect, then you might want to take a look at this article: Tweetie's developer explains pretty thoroughly the method he used to get that smooth scrolling you're after. This has become a bit easier since then, though: CALayer has a shouldRasterize property that basically flattens its sublayers into a bitmap, which can then—as long as nothing changes inside the layer—give you much better performance when animating the layer around, as UITableView does when you scroll it. In this case, you'd probably apply that property to your individual UITableViewCells' layers.
My guess is that the delay is from storing images in Core Data. Core Data is usually not a good way to store large blobs of data.
A better solution would be to store the images as individual files on disk, using an album id to identify each image. Then you would setup an in memory cache to store the images in RAM for fast loading into your UIImageViews. The loading of the images from disk to RAM would ideally need to be done on a background thread (e.g. try performSelectorOnBackgroundThread) so that the I/O doesn't bog down the main thread (which will impact on your scrolling performance).
Addendum 1
If you're only calling -drawRect: once per cell load (as you should be), then the problem might be the scaling of the images. Scaling an image by drawing it in code using drawInRect will use CPU time, so an alternative approach is to scale the images as you receive them from the iPod library (if you need them in multiple sizes, save a version in each size you require). You may need to do this on a background thread when you import the data to avoid blocking the main (UI) thread.
One alternative thing to consider is that UIImageView may do it's scaling using Core Animation which would mean it is hardware accelerated (I'm not sure if this is actually the case, I'm just guessing). Switching to a UIImageView for the image would therefore get rid of the CPU burden of image scaling. You would have a slight increase in compositing overhead, but it might be the easiest way to get closer to "optimum" scrolling performance.
At this point your best bet is to use Instruments (previously Shark) to try and find bottlenecks in your code.

TTTableImageItem doesn't load the image until scroll

I'm using the three20 project for my iPhone app. I've narrowed my problem down and I'm now just trying to re-create the 'Web Images in Table' example that comes with the project. I've copied the code exactly as in the project, with the exception that I do not use the TTNavigator (which the example does) but I am adding my TTTableViewController manually to a tabBar.
The problem is as follows; the images in the table should load automatically from the web, like in the example. But they only load after I scroll the table up and down.
In the console it clearly says it is downloading the images, and you see the activity indicator spinning like forever.. And unless I scroll up and down once, the images will never appear.
Anyone? Thanks in advance.
P.S:
If I'm using this code in any random UIView, It also doesn't work (only shows a black square):
TTImageView* imageView = [[[TTImageView alloc] initWithFrame:CGRectMake(30, 30, 100, 100)] autorelease];
imageView.autoresizesToImage = YES;
imageView.URL = #"http://webpimp.nl/logo.png";
[self.view addSubview:imageView];
If I put this code in my AppDelegate (right onto the window), it does work .. strange?
POSSIBLE SOLUTION:
Although I stopped using TTImageView for this purpose, I do think I found out what the problem was; threading (hence accepting the answer of Deniz Mert Edincik). If I started the asynchronous download (because basically that is all the TTImageView is, an asynchronous download) from anywhere BUT the main thread, it would not start. If I started the download on the main thread, it would start immediately..
Sounds like a threading problem to me, are you creating TTImageView in the main runloop?
I find one interesting thing. When I use combination TTTableViewController, TTTableViewDataSource and TTModel I have same problem with loading TTImageView. My problem was, that my implementation of Model methods 'isLoading' and 'isLoaded' don't return proper values after initialization of model. That forces me to call reload on model manualy in 'viewDidAppear' method and that causes image loading problem. So I repair my 'isLoading' and 'isLoaded' methods to both return 'NO' after Model init, and everything is fine.
When an image finishes loading try sending a reloadData message to the table view. This forces the table to recalculate the size of the rows and redraw the table. Just be careful that you don't start downloading the image again in response to this message.
I've written something similar to this where an image view will load its own image from the web.
Im my experience, when the image had loaded successfully but was not shown in its view, it was a case that the cell needed to be told to redraw.
When you scroll the table view, the cells are set to redraw when the come onscreen, which is why they appear when you scroll.
When the image loads, tell the cell that it is sitting in to redraw by sending it the message setNeedsDisplay.
That way, when the image finishes downloading, the cell its sitting in (and only that cell) will redraw itself to show the new image.
It's possible that you might not need to redraw the entire cell and might be able to get away with simply redrawing the image view using the same method call. In my experience, my table cells view hierarchy was flattened, so I had to redraw the whole cell.
I don't have an answer for what you want to do, but I will say that this is considered a feature, and the expected behavior. You use TTImageView in UITableView when you want to do lazy loading of images. TTImageView will only load the images whose frames are visible on the screen. That way, the device uses its network resources to download images that the user has in front of them, rather than a bunch of images that the user isn't even trying to look at.
Consider having a long list that may contain a couple hundred thumbnail images (like a list of friends). I know from experience that if you kick off 100+ image requests on older devices, the memory usage will go through the roof, and your app will likely crash. TTImageView solves this problem.
This is a thread problem. You can load the images by including the line:
[TTURLRequestQueue mainQueue].suspended = NO;
in - (void)didLoadModel:(BOOL)firstTime.