The right approach to loading dynamic content into a UITableView in iOS - iphone

ok, I've read tons of bits and pieces on the subject of loading dynamic content (from the web) into a UITableView and the problem with calculating cell height upfront. I've tried different simple implementations but the problem persists...
Assuming I need to read a JSON file from the web, parse it into 'item' objects, each with variable size image and various text labels, here is what I believe would be the right approach to avoid long hang time of the app while everything is loading:
on app load read JSON file and parse into items array
provide only small part of the items array to the tableview (about 10 items) - since I need to load the images associated with each item to calculate cell height - I don't want the view to go through the whole items list and load all images - this hangs the app until every image is loaded
display the tableview with the available cells (assuming I load a few 'spare' ones, user can even scroll to more items)
in the background using Grand Central Dispatch download images for all/some of the remaining items and then reload the tableview with the new data (repeat step 4 if item list is very long)
Step 2 above is necessary since I have no way to calculate the cell height without loading the images first, and since tableview first calculates height of all cells it may take a very long time to download all images for all items.
Would you say this is the right approach? am I missing something?

Why don't you standardize in the tableview the image size. Then you can manipulate the images as they are displayed to fit the size. Offer a way to view the image if selected. The Quartzcore framework will allow you to take your original images and size them.
I only suggest this because it would make your tableview look more appealing with uniform picture sizes than with random ones.

You're right that the show must go on, even while your data is still loading. So you'll want to design a cell that looks attractive before it has the correct image. A common approach is to ship a default image and format the cell so that looks good.
To handle the height, the tableView datasource protocol can ask you how tall a cell should be. The way to answer is, in pseudo code:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
id myModelElement = [self.myModel objectAtIndex:indexPath.row];
UIImage *image = myModelElement.image;
if (!image) image = self.placeholderImage;
return kFIXED_HEIGHT + image.size.height;
}
You're also correct that you'll want to load the images asynchronously. See my answer here for a very simple approach to that. No GCD, not even a table row reload.

Related

UITableView doesnt scroll smoothly, because it loads the cell again [duplicate]

This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
Lazy load images in UITableViewCell
Ive got a problem with my UITableView cells and it is that my cells are reloading each time they go out from the screen. My app loads images from a url and place them in a ImageView in the left part of the cell, so it makes the UITableView scroll really slow.
I was wondering if there is a way to:
1.Load all the cells at once.
2.Disable the reload of the cells.
In this way the TableView could scroll smoothly.
Each cell is composed of three subViews. Two are used for the title and description label and the last one for the image. The contents come from a NSXMLParse so loading this cells takes time.
Thanks in advance.
Instead of using uiimageview use sdwebimage. It will help you I think.
The three ways I can think of to improve the scrolling are to:
Not load the images in the main thread, just stick in a place holder and have them filled in when the data finally becomes available (lazy loading as mentioned in the comment).
Cache the images yourself so when the cell is viewed later you already have the data to display.
Pre-fetch anticipated images into your cache for the cells above and below the displayed rows.
Of course these are not mutually exclusive and should likely be combined to get the smoothest user interface.

What is an acceptable FPS for scrolling, and what are tips for improving performance?

I see in many WWDC video's that says you want to achieve 60.0 FPS as close as possible to get a better smooth scrolling experience. I have a UIScrolLView which loads up image and a couple of table view's at once. Currently I am getting 30 FPS. This is half of what the recommended FPS. Just wondering what FPS do you guys typically get for a table view/scroll view that loads up images and other heavy stuff/rendering stuff.
Any other tips for optiziming FPS? I've spend the past week till now firing up Instruments using the time profiler, allocations, and core animation tool to optimize as much as I can.
Just to clarify a bit on what I have. I have a masonry/waterfall/pinterest style layout on the iPad. So it's not just a regular UITableView. It's a UIScrollView that fills out the whole screen, and is filled with a couple of UIView's. Each of this view has a 150x150 UIImageView and a UITableView and also it has some attributed label, drawn using Core Text. So at one glance when you see the screen, you can see 5-8 table view at one shot, each cell again has a UIImageView and then each cell renders attributed label drawn using core text.
So you can just image how deep and complicated this is. This is not just a regular table view with a UIImageView. I know how to get 60 FPS with just one UITableView in an iPhone with a UIImage. The concept is to load images asynchrounously and not to block the main thread as much as possible.
EDIT:
It seems that the problem here is the UITableView that I have inside my view.. when I remove that from the UIView I get really smooth scrolling..
I uploaded a sample project which is a simpler version of what I have, but it clearly shows the problem. The link is here
Many things affect render performance, here are some items you can check:
Profile - You said you already did this, so great job! Unfortunately profiling is often overlooked, even though it can reveal unexpected problems. In one app I was working on a calendar with different cells representing dates. At first scrolling between cells slow, which was unexpected. I thought maybe it was drawing a few cells too many. After profiling I found that [NSCalender currentCalender] was using 85% of my CPU time! After fixing that everything scrolled great!
Images - Large images put a lot of load in CoreGraphics. Scrolling especially requires a lot of draw operations to move them around. One tip is to scale images on the device as little as you can, that makes CoreGraphics' job a lot easier. If an image is twice as large as the view displaying it, resize the UIImage before displaying it in the view. iOS devices handle PNGs best. They are compressed by a tool (pngcrush) at compile time and iOS has special hardware for rendering them.
Edit: JPGs are probably a better option for photos. iOS devices have dedicated JPG decoders as well.
Custom Drawing - If possible, cutback on the amount of custom CGContext drawing you do. Lots of custom drawing has negative effects on animation speed. I would considering using an image over complex custom drawing, if possible.
Cull - Only draw things you need to. UITableView automatically unloads and loads cells as they appear, so this is done for you, but any custom CGContext drawing should only be done when that part is visible. Also automatic view shadows can be very slow in my experience.
Reuse - Use the reuse identifier on UITableView, this will allow UITableView to reuse cell objects rather than reallocating as it scrolls - look at the answer to this question. Also reuse UIImages rather than allocating multiple for the same file. imageNamed caches images automatically but imageFromContents of file does not.
Create your own - You could create your own grid view class that culls it's subviews views hidden off screen, and scrolls with lazy content loading. By writing a custom solution you can fully control the process and create a design optimized for the usage context. For most use cases you will have a hard time building something better than the Apple standard, but I have seen it done in specific cases.
Last resort - Reduce the size of the offending view (improves filtrate), break content into multiple pages, downsize images, cut out older devices that don't perform as well. I would settle for 30 FPS before sacrificing most of that stuff. Devices will continue to get faster, older devices will be eliminated, and your app will gradually get faster.
I get close to 60 fps with my UITableViewController where the table contains about 2000 cells and each cell pulls an image from the web. The trick is to lazy load the images as you need them. This sample code from Apple is pretty helpful.
The general idea is to keep the UI responsive by not blocking the main thread. Perform downloads and other time-consuming tasks on another thread.
I would do something called Lazy Loading, which doesn't load the images until they are actually seen.
Here's a great example on how to do so: http://www.cocoacontrols.com/platforms/ios/controls/mhlazytableimages
Good Luck!
What I've done is to use NSCache. I've created a small class with properties that conforms to the NSCache data protocol (its really easy to do). So what I do is create a relationship between each cell in the main table and various things worth caching: NSAttributed strings, images etc - really anything that takes work to create. I don't preload it but you could.
When you are asked to provide a cell by the tableview, look in your cache for your primary object. If there, pull all all the objects you need. If the cache does not have the object, then get the data the old fashion way, but before you finish, save it in the cache too.
This really helped me reduce "stutter" when scrolling the cell. Also, do NOT animate anything in the cell - that kills performance. Everything should be fully rendered.
Another thing to remember - make sure ever view which can be set to opaque has its property set to YES. That for sure helps the system render the cell (including the backgound view if you use one.)
EDIT:
So you provided information that included UITableViews may the root problem. So two suggestions:
1) Can you step back and figure out how to make the scrollView a single UITableView? With table headers and footers, and section headers and footers, and even the ability to essentially make a cell a floating view, can't you figure out how to rearchitect what you have?
2) So you decide no to suggestion 1. Then, do this. Think of the space used by the tableview as being a container view. Whenever the tableview is edited, take an image snapshot of it and keep this image around. As soon as the user starts to scroll, swap the tableViews out for the images. When the scrollView stops swap the UITableView back in. This of course will take some fine tuning. In fact, you could probably overlay an opaque image snapshot over the table (which will hide it and prevent it from being asked to draw itself) during scrolling.
the human eye sees at about 60 FPS, so that's why it's recommended, but 30 FPS will also appear very smooth, especially when a regular user is viewing it, as opposed to you trying to find as much to fix as possible. This is obviously dependent on how fast the scrolling goes, if the difference from frame to frame is a movement of a few pixels, 30 FPS will do just fine, but faster movement will require a higher FPS to appear smooth
There are a few things you can do in general to get better table view performance:
1) Switch to Loren Brichter's method of drawing UITableViewCell's (for lack of a better link: http://www.therefinedgeek.com.au/index.php/2010/12/21/fast-scrolling-uitableview-updates-for-ios-4-2/)
Basically, all his code does is render all your cell content as one opaque UIView, which UITableView (and CoreGraphics) can very quickly blast onto a UITableViewCell
If you don't want to do all your cell design in drawRect:, you can still use nibs, but:
Make sure every subview is marked opaque
Don't have any transparent/semi-transparent subviews
Don't have any images with an alpha channel != 1.0f.
2) Don't let UIImageView do any scaling to display your image, give it a correctly-sized UIImage
3) If you're using iOS 5 and above, you can register a nib for a particular cell identifier. That way, when you call [tableView dequeueReusableCellWithIdentifier:], you are guaranteed to get a cell. Cell allocation is faster (according to Apple), and you get to write less code:
- (void)viewDidLoad {
UINib *nib = [UINib nibWithNibName:#"MyCell" bundle:nil];
[self.tableView registerNib:nib forCellReuseIdentifier:#"MyCellIdentifier"];
}
// ...
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *cellIdentifier = #"MyCellIdentifier";
MyCell *cell = (MyCell *)[tableView dequeueReusableCellWithIdentifier:cellIdentifier];
// Commented out code is no longer needed
//if (cell == nil) {
// cell = [[MyCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
//}
// setup cell
return cell;
}
4) Displaying images downloaded from the web
Display a default image (something to look at while the real image is downloading)
Start the download on a separate thread (hint: use GCD's dispatch_async())
When the image comes in, cache it (hint: NSCache), and display it on the cell
Do all image downloading/caching off the main thread; the only thing you should be doing on the main thread is setting the image (remember UI code HAS to be on the main thread!)
You'll probably want to write an async-capable UIImageView (or use an existing library).
Stay away from EGOImageView, even though it has async downloading, it does cache lookup (which happens to be on disk, so that means costly disk IO) on the main thread before dispatching to a background thread for the download. I used to use it, but I ended up writing my own set of classes to handle this, and it's significantly faster.
-
Just follow these, and stuff other ppl have written here, and you'll have table views that scroll like glass in no time :)
You want 60fps, but 30 fps doesn't look too terrible in actuality. But I would try to achieve 60fps for a smoother look while scrolling.
There are many Performance Improvement possibilities, that are also shown by various tutorials
While 60 FPS is the ideal, games like Halo run very prettily in 30 FPS. The battlefield chaos in Halo probably involve more surprising, rapid motion than most lists, even complex ones like yours!

Memory Handling for thumbnail images in UITableViewCells

Short Explanation
Currently I have a UITableView which contains cells of videos from a website. In addition each cell (which represents a video) has a specific thumbnail image. These images are downloaded asynchrounously using NSURLConnection (so I do not have to worry about threading myself). When these image objects have downloaded they simply notify the UITableView to refresh the cell it belongs to.
My Problem
As standard there will be fetched 10 new videos to the UITableView each call. This means that the user is allowed to push a cell at the bottom of the UITableView to request 10 new videos and so forth. The problem here is that there will quickly be a whole lot of memory usage because data for all thumbnail images (in the UITableView) will still exists no matter what.
Is there a smart way in which you can de-allocate image objects that aren't currently in the view?
Also, the UITableView simply renders all its cells directly from an array I have of all video objects ever fetched from the website. This means there is no limits as to how many times the user can request new videos, making this array bigger and bigger all the time. Is it correct to keep these kind of arrays in memory all the time? Or should you instead delete the ones which goes out of the view and request them again later on?
Thank you in advance
If you're using the standard method approach of queuing and de-queuing cells in your tableview, then you really should only be using a handful of cell views (and their related thumbnails) - eg, the total number of cell views allocated should never be more than the maximum number viewable on screen, and by extension, the number of allocated/references to thumbnail images should not exceed the total number of cells. Each time you present a row representing a new movie, the imageview is as likely reused as it is new.
What you want to consider is that your data array (an array of your downloaded clips, with potentially unlimited objects therein) is completely discrete from your tableview's array of cells, which is going to be a relatively small number.
My advice would be to conceive a download cache of images. Basically, as your thumbnails are downloaded, assign each a unique string id, and write the actual image file out to disk. Assign the unique ID string to your model object. When your tableview prepares to present an image, locate the image for the cell using the id string, load the image from disk, and populate the image view. As long as you're not retaining the image, the image should be released when the cell presenting it goes off-screen. You can additionally empty the cache of loaded images under low memory conditions.
If you create your own class VideoTableViewCell as a subclass of UITableViewCell and you give your cell a property for an image this image will be released as soon as the cell goes off the screen. The cell and the image will be recreated as soon as the Cell come back on the view.
For the videos I would store all downloaded video files in the documents folder and store there url in an array. If a cell comes visible or is clicked simply load the file from the hard drive.

Static cell from Nib file is not shown

I have a Nib file containing grouped table view and a cell. The cell is connected to UITableViewController through outlet. In cellForRowAtIndexPath I just return that cell and I can see a single cell in the table view. But when I change the row count of table to 2 and want to show the same cell, then I can see only one, it appears that the second cell is intended to be there, as the lower corners of the visible cell are not rounded, however, it's not there.
If I create a second cell object in nib file, second outlet and return it as second row, the it appears fine. My cell has identifier specified in IB.
Does it mean I can't re-use cell object for more than one row?
In the same way as you would need one instance of a UIButton for each visible button in your view, you will need one instance of your cell for each visible row.
The common pattern to manage this, is to ask the tableview for a previously instantiated cell that is no longer needed (dequeueReusableCellWithIdentifier:), and then return that cell. If the table view does not have any reusable cells, you have to instantiate a new one from your nib file.
There are many examples on this around the web, and you can also find some here at SO, ex in this answer.
I would recommend that you read through Apples TableView Programming Guide, which also contains a section on loading cells from nibs.
UPDATE:
An attempt on explaining the TableView and reuse of cells in a different way.
Lets say we have a large gallery with
old paintings. Thousands of paintings.
The gallery has just one display room,
though, and it has walls for just ten
paintings. The gallery manager has to
switch paintings now and then when the
visitors get bored and want to see
some new paintings.
Every displayed painting needs a
frame. Without a frame, it can't be
put on a wall. Frames are expensive to
make, and take up a lot of space. The
frame maker guy want have time nor
money to build the thousands of frames
needed.
He finds out that he want be needing
frames for all the paintings that is
not shown at the moment. He would only
need ten frames for the currently
displayed paintings. When the gallery
manager takes down a painting, the
frame maker stores the frame, and when
the gallery manager put up a new
painting and asks the frame maker for
a frame for it, the frame maker
returns the frame from the previous
painting again.
One day, the
needed-space-between-paintings-regulations
gets changed for no good reason. The
gallery manager is able to put up two
more pictures in the display room. He
picks two paintings from the store
room, and asks the frame maker for
frames. The frame maker has no spare
frames, and need to make two new
frames.
Now, lets say that the gallery is a TableView, and all the paintings are rows of data. The display room with space for ten visible paintings, is the screen, with space for ten visible rows. Each visible row would need a cell, just like each displayed painting would need a frame.
In the end, you shouldn't care that much about saving resources by reusing one cell. That's TableViews responsibility. It's an implementation detail of the TableView how many cells is needed and how it is used. The protocol defines how you can ask the TableView for an reusable cell, and the documentations states that you should. That should be enough. Demo projects shows that TableView can manage very large amounts of data. If your projects struggles with performance because of instantiating 10-20 cells from nib, you probably got some problems with your nib file or something. There are some discussions, though, about the performance of loading from nib versus building cells in code. It may be interesting to you.
I had some very weird behavior that sounds very much like what you are describing some time ago.
Eventually I found that the problem was that I had just added a table view cell to a xib which contains other items such as the parent table view and controller. What I had to do was create a seperate xib for each table view cell individually. I think the issue was that loading the table view cell from an incorrectly built xib was confusing the issue.
As Vegar said there are a lot of tutorials on how to do it.

Adding a dynamic-height UITableView into a scrolling view?

Hello all – I'm getting into iPhone development and have hit my first confusing UI point. Here's the situation:
My app is tab-based, and the view that I'm confused about has a static featured content image at the top, then a dynamic list below into which X headlines are loaded. My goal is to have the height of the headline table grow as elements are added to it, and then to have the whole view scroll (both featured image on top and headline list below). So, I guess my question comes in two parts:
1) First, how do you set up a dynamic-height table view that will grow as cells are added to it. So far I've only been able to have my tables handle their own scrolling.
2) Then, what is the root NIB view that the featured image and the table should live in to enabled scrolling? I've dropped oversized content into a UIScrollView now, although did seem to have any success with having it automatically scroll.
Thanks in advance for any help on this subject!
To the first:
As i understand your situation:
You want to add a image to the top of the UITableView and the image should scroll with the UITableView, shouldn't?
The UITabeView has a property called tableHeaderView. It's just a view, so you can set a UIImageView to it.
(I have no xCode at the current time, you need to edit the code)
UIImage *image = [UIImage imageNamed:#"myCoolPic.png"];
UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
imageView.frame =CGRectMake(0,0,width,height);
tableView.tableHeaderView = imageView;
[imageView release];
What you're asking is probably doable with Interface Builder (or not, I don't know) but I know the code way to do it.
To change the height of the table all you do is set the frame of the UITableView object. The default height of a UITableViewCell is 44 I believe, so set it to multiples of that depending on how many cells you have. Of course your cells can be any height so you will need to keep track of what you report in heightForRowAtIndexPath and set the table frame accordingly.
UITableView will certainly live in a UIScrollView and both components can scroll. The table view needs to become a subview of the scroll view, so does the image. Then you will scroll the table if you drag on it directly or scroll the scroll view if you drag the image or the scroll view.
For the first question, I'm a little confused by the way you ask it: "how do you set up a dynamic-height table view that will grow as cells are added to it." Table views have a function that it calls before the table is fully loaded with data called "numberOfRowsInSection." So the number of cells is based on that function, and should you update the variable used to determine the return value of that function (usually [myArray count]) it should automatically find the right size for the whole table.
However, variable height cells are something that I found kinda tricky and I've solved it using the following:
There are some UIKit NSString additions that you might find useful.
http://developer.apple.com/iphone/library/documentation/uikit/reference/NSString_UIKit_Additions/Reference/Reference.html
Particularly the sizeWithFont: functions.
Table views also have a 'heightForRowAtIndexPath:' function that is called 'numberOfRowsInSection' amount of times. Each call determines the height of the cell at the indexpath.
So, for example: (assuming myArray is an array of NSStrings)
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { return [[myArray objectAtIndex:indexPath.row] sizeWithFont:myFont];}
This will return a height based off of your actual data, piece by piece. There are other functions to specify how the text wraps and truncates, etc. as well.
It doesn't feel like a great solution because you end up fetching your data twice, once to determine the height, and then again when you configure the cell in 'cellForRowAtIndexPath:' However, it does work!
I've learned a lot in the past few weeks and have gone through a few iterations of addressing this problem. My first solution was to manually measure the table height, then set the table rect to display at that height, and finally to set the scrollView's content rect to encompass the the table and top feature. What that solution did basically work, I started encountering some display issues when branching out into new views with different toolbar configurations. It seemed that my manual frame size was interfering with iPhone's native content scaling.
So, I scrapped the manual sizing and went to just making that top feature block be a custom table cell that displayed within its own section at the top of the table. I made a hard logic definition that section 0 only had one table cell, and that cell was my custom layout that I linked in through Interface Builder. I was then able to get rid of ALL my messy custom scaling logic, and the whole system is cleaner, smoother, and works reliably.