Ok, This is an old issue almost every iOS developer has struggled with. Many answers on the subject are a available.
However I still haven't found a really practical, generic solution for calculating a UITableViewCell's height in tableView:heightForRowAtIndexPath:.
I understand how the layout mechanism of UITableView works (it calculates the height of the whole table before actually laying out any cells).
I also understand that one basically has to anticipate everything that will happen in UITableViewCells layout method using for example sizeWithFont:... methods on NSString.
I assume that the style of the cell is UITableViewCellStyleSubtitle to begin with (we can get more generic later!).
These would be my requirements:
the cell might be in a grouped or plain tableView
the system version might be iOS 6 or iOS 7+
an image might or might not be set in cell.imageView
an accessory view might or might not be set
cell.textLabel might or might not contain text.
cell.detailTextLabel might or might not contain text.
fonts might be customized
the labels might be multi-line
the table width is arbitrary (portrait, landscape, iPad...)
the number of cells is limited (probably some dozen)
In most cases this kind of dynamic cell would be used in short lists (maybe some kind of detail view) so I think it would be viable to pre-calculate the cells like described here: https://stackoverflow.com/a/8832778/921573
I am looking for an implementation for calculating the cell height that satisfies the given requirements.
I'd be very happy if anyone could share some insights & experience - thanks in advance!
You could keep a single dummy cell in your table view's delegate and let it calculate its own required size.
The best way to make custom cells with different heights is:
make a NSMutableArray for cells:
#property (nonatomic,retain)NSMutableArray* cellsArray;
then in m.:
the first method which call is
-(float)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
make your cell here:
-(float)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
if (!self.cellsArray) {
self.cellsArray = [[NSMutableArray alloc]init];
}
ListCell* cell = [[ListCell alloc] initWithFrame:CGRectMake(x, y, width, height)];
// IMPLEMENT your custom cell here, with custom data, with custom,different height:
// after add this cell to your array:
[self.cellsArray addObject:cell];
// IMPORTANT STEP: set your cell height for the new height:
cell.frame = CGRectMake(cell.frame.origin.x, cell.frame.origin.y, 303, yourLastItemInCell.frame.origin.y + yourLastItemInCell.frame.size.height);
return cell.frame.size.height;
}
after you got the custom cells with different heights :
#pragma mark - conform to delegates
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 10;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
return self.cellsArray[indexPath.row];
}
I hope it helps !
EDIT:
tableView = [[UITableView alloc] initWithFrame:frame style: UITableViewStylePlain];
tableView.delegate = self;
tableView.dataSource = self;
tableView.backgroundColor = [UIColor clearColor];
tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
This question already has answers here:
tableFooterView property doesn't fix the footer at the bottom of the table view
(10 answers)
Closed 8 years ago.
I have a UITableView with a footer, filled with a tabBar in a custom view, done using the following code:
- (CGFloat)tableView:(UITableView *)tableView
heightForFooterInSection:(NSInteger)section {
//differ between your sections or if you
//have only on section return a static value
return 49;
}
- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section {
if(footerView == nil) {
//allocate the view if it doesn't exist yet
footerView = [[UIView alloc] init];
[footerView addSubview:self.tabBarView];
}
//return the view for the footer
return footerView;
}
Which is working lovely, apart from when the table has less rows than are needed to fill the screen, this causes the footer to move up the screen, as the table no longer creates empty rows, due to having a footer.
So, does anyone know of a way to either lock the custom footer to the bottom of the screen, or, to make the tableView create empty rows as it used to do?
Thanks!
Gareth
Unfortunately I don't think there is an easy way to do this, other than some view hierarchy trickery. When the contentSize of your UITableView is less than the frame size, you assign the footer view to self.view and position manually. When the contentSize of your UITableView is greater than the frame size, you use viewForFooterInSection. Let me know if this isn't clear or if you'd like to see some sample code on how to do this.
I have the same problem for uitableview controller, I solved this by adding view in the window and remove this object in viewWillDisappear.
My Solution link
How do i add a space between 2 sections in a tableView.
Once i added the following code in the
titleForHeaderInSection
if (section==0)
return #"sec 1";
else return #"sec 2";
then, the following
viewForFooterInsection
vi = [[UIView alloc]initWithFrame:CGRectMake(0,0,300,100) ];
la = [[UILabel alloc] initWithFrame:CGRectMake(0,0,300,50)];
la.text=#"............long text..";
[vi addSubView:la];
return vi;
When i make the label Height to 30, it displays 1/2 of the text. Therefore i will need to increase the Height of the label. Then when i increase the Height the 2nd sections Header overlaps. How can i solve this ?
You have to specify the footer height in your tableview delegate, like:
- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section
{
// Switch here if you like different heights for footers
return 50.0f;
}
The answer lies in the tableView's style. The style that you see is UITableViewStylePlain, where the sections are not separated. What you are looking for is UITableViewStyleGrouped. This can be given in two places.
If you are creating a tableView, use its initializer
UITableView *tableView=[[UITableView alloc] initWithFrame:someRect style:UITableViewStyleGrouped];
Or, if you are creating a UITableViewController
UITableViewController *tbvc=[[UITableViewController alloc] initWithStyle:UITableViewStyleGrouped];
I have a UITableView with two sections. It is a simple table view. I am using viewForHeaderInSection to create custom views for these headers. So far, so good.
The default scrolling behavior is that when a section is encountered, the section header stays anchored below the Nav bar, until the next section scrolls into view.
My question is this: can I change the default behavior so that the section headers do NOT stay anchored at the top, but rather, scroll under the nav bar with the rest of the section rows?
Am I missing something obvious?
Thanks.
The way I solved this problem is to adjust the contentOffset according to the contentInset in the UITableViewControllerDelegate (extends UIScrollViewDelegate) like this:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGFloat sectionHeaderHeight = 40;
if (scrollView.contentOffset.y<=sectionHeaderHeight&&scrollView.contentOffset.y>=0) {
scrollView.contentInset = UIEdgeInsetsMake(-scrollView.contentOffset.y, 0, 0, 0);
} else if (scrollView.contentOffset.y>=sectionHeaderHeight) {
scrollView.contentInset = UIEdgeInsetsMake(-sectionHeaderHeight, 0, 0, 0);
}
}
Only problem here is that it looses a little bit of bounce when scrolling back to the top.
{NOTE: The "40" should be the exact height of YOUR section 0 header. If you use a number that is bigger than your section 0 header height, you'll see that finger-feel is affected (try like "1000" and you'll see the bounce behaviour is sort of weird at the top). if the number matches your section 0 header height, finger feel seems to be either perfect or near-perfect.}
You can also add a section with zero rows at the top and simply use the footer of the previous section as a header for the next.
Were it me doing this, I'd take advantage of the fact that UITableViews in the Plain style have the sticky headers and ones in the Grouped style do not. I'd probably at least try using a custom table cell to mimic the appearance of Plain cells in a Grouped table.
I haven't actually tried this so it may not work, but that's what I'd suggest doing.
I know it comes late, but I have found the definitive solution!
What you want to do is if you have 10 sections, let the dataSource return 20. Use even numbers for section headers, and odd numbers for section content. something like this
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if (section%2 == 0) {
return 0;
}else {
return 5;
}
}
-(NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
if (section%2 == 0) {
return [NSString stringWithFormat:#"%i", section+1];
}else {
return nil;
}
}
Voilá! :D
Originally posted Here, a quick solution using the IB. The same can be done programmatically though quite simply.
A probably easier way to achieve this (using IB):
Drag a UIView onto your TableView to make it its header view.
Set that header view height to 100px
Set the tableview contentInset (top) to -100
Section headers will now scroll just like any regular cell.
Some people commented saying that this solution hides the first header, however I have not noticed any such issue. It worked perfectly for me and was by far the simplest solution that I've seen so far.
There are several things that need done to solve this problem in a non-hacky manner:
Set the table view style to UITableViewStyleGrouped
Set the table view backgroundColor to [UIColor clearColor]
Set the backgroundView on each table view cell to an empty view with backgroundColor [UIColor clearColor]
If necessary, set the table view rowHeight appropriately, or override tableView:heightForRowAtIndexPath: if individual rows have different heights.
I was not happy with the solutions described here so far, so I tried to combine them. The result is the following code, inspired by #awulf and #cescofry. It works for me because I have no real table view header. If you already have a table view header, you may have to adjust the height.
// Set the edge inset
self.tableView.contentInset = UIEdgeInsetsMake(-23.0f, 0, 0, 0);
// Add a transparent UIView with the height of the section header (ARC enabled)
[self.tableView setTableHeaderView:[[UIView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 100.0f, 23.0f)]];
Just change TableView Style:
self.tableview = [[UITableView alloc] initwithFrame:frame style:UITableViewStyleGrouped];
UITableViewStyle Documentation:
UITableViewStylePlain-
A plain table view. Any section headers or footers are displayed as inline separators and float when the table view is scrolled.
UITableViewStyleGrouped-
A table view whose sections present distinct groups of rows. The section headers and footers do not float.
Select Grouped Table View style from your tableView's Attribute Inspector in storyboard.
Set the headerView of the table with a transparent view with the same height of the header in section view. Also initi the tableview with a y frame at -height.
self.tableview = [[UITableView alloc] initWithFrame:CGRectMake(0, - height, 300, 400)];
UIView *headerView = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, width, height)] autorelease];
[self.tableView setTableHeaderView:headerView];
Change your TableView Style:
self.tableview = [[UITableView alloc] initwithFrame:frame style:UITableViewStyleGrouped];
As per apple documentation for UITableView:
UITableViewStylePlain- A plain table view. Any section headers or
footers are displayed as inline separators and float when the table
view is scrolled.
UITableViewStyleGrouped- A table view whose sections present distinct
groups of rows. The section headers and footers do not float. Hope
this small change will help you ..
I found an alternative solution, use the first cell of each section instead a real header section, this solution don't appears so clean, but works so fine, you can use a defined prototype cell for your headers section, and in the method cellForRowAtIndexPath ask for the indexPath.row==0, if true, use the header section prototype cell, else use your default prototype cell.
Now that the grouped style looks basically the same as the plain style in iOS 7 (in terms of flatness and background), for us the best and easiest (i.e. least hacky) fix was to simply change the table view's style to grouped. Jacking with contentInsets was always a problem when we integrated a scroll-away nav bar at the top. With a grouped table view style, it looks exactly the same (with our cells) and the section headers stay fixed. No scrolling weirdness.
Assign a negative inset to your tableView. If you have 22px high section headers, and you don't want them to be sticky, right after you reloadData add:
self.tableView.contentInset = UIEdgeInsetsMake(-22, 0, 0, 0);
self.tableView.contentSize = CGSizeMake(self.tableView.contentSize.width, self.tableView.contentSize.height+22);
Works like a charm for me. Works for section footers as well, just assign the negative inset on the bottom instead.
I add the table to a Scroll View and that seems to work well.
Check my answer here. This is the easiest way to implement the non-floating section headers
without any hacks.
#LocoMike's answer best fitted my tableView, however it broke when using footers as well.
So, this is the corrected solution when using headers and footers:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return (self.sections.count + 1) * 3;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
if (section % 3 != 1) {
return 0;
}
section = section / 3;
...
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
if (section % 3 != 0) {
return nil;
}
section = section / 3;
...
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
if (section % 3 != 0) {
return 0;
}
section = section / 3;
...
}
- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section
{
if (section % 3 != 2) {
return 0;
}
section = section / 3;
...
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
if (section % 3 != 0) {
return nil;
}
section = section / 3;
...
}
- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section
{
if (section % 3 != 2) {
return nil;
}
section = section / 3;
...
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
int section = indexPath.section;
section = section / 3;
...
}
Swift version of #awulf answer, which works great!
func scrollViewDidScroll(scrollView: UIScrollView) {
let sectionHeight: CGFloat = 80
if scrollView.contentOffset.y <= sectionHeight {
scrollView.contentInset = UIEdgeInsetsMake( -scrollView.contentOffset.y, 0, 0, 0)
}else if scrollView.contentOffset.y >= sectionHeight {
scrollView.contentInset = UIEdgeInsetsMake(-sectionHeight, 0, 0, 0)
}
}
I've learned that just setting the tableHeaderView property does it, i.e. :
tableView.tableHeaderView = customView;
and that's it.
My app needs to have variable height table cells (as in each table cell differs in height, not that each cell needs to be able to resize itself).
I have a solution that currently works, but it's kludgy and slow.
My Current Solution:
Before the table cells are rendered, I calculate how high each cell needs to be by calling sizing methods such as -sizeWithFont:constrainedToSize: on its data. I then add up the heights, allow for some padding and store the result with the data.
Then when my UITableViewDelegate receives the -tableview:heightForRowAtIndexPath: I work out which item will be rendered for that cell and return the height that I calculated previously.
As I said, this works, but calling -sizeWithFont:constrainedToSize: is very slow when you're doing it for hundreds of items sequentially, and I feel it can be done better.
So for this to work, I had to maintain two parts of code - one that would calculate the cell heights, and one that would actually draw the cells when the time comes.
If anything about the model item changed, I had to update both of these chunks of code, and now and again they still don't even match up perfectly, sometimes resulting in table cells that are slightly too small for a given item, or too large.
My Proposed Solution:
So I want to do away with the precalculating the cell height. A) because it breaks the MVC paradigm and B) because it's slow.
So my cell draws itself, and as a result, ends up with the correct cell height. My problem is that I have no way of telling the table view the height of the cell before its drawn - by which time its too late.
I tried calling -cellForRowAtIndexPath: from within -tableView:heightForRowAtIndexPath: but this gets stuck in an infinite loop, since the first calls the second at some point, and vice versa (at least this is what I saw when I tried it).
So that option is out of the question.
If I don't specify a size in the height for row delegate method, then the table view goes screwwy. The cells are the perfect height, but their x position is that of cells of fixed heights.
Messed Table Cells http://jamsoftonline.com/images/messed_table_cells.png
Notice how the bottom cell is the correct size - it's just overlapping the previous cell, and the previous cell overlaps its previous, and so on and so forth.
Also using this method, while scrolling there is some artifacting occurring which I think may be related to the reuse identifier for the cells.
So any help here would be gratefully appreciated.
Here's what I use. NSString has a method that will tell you the dimensions of a textbox based on the font information and the height/width constraints you give it.
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *text = [self getTextForIndexPath:indexPath];
UIFont *font = [UIFont systemFontOfSize:14];
CGSize size = [self getSizeOfText:text withFont:font];
return (size.height + 11); // I put some padding on it.
}
Then you write a method pull the text for this cell...
- (NSString *)getTextForIndexPath:(NSIndexPath *)indexPath
{
NSString *sectionHeader = [self.tableSections objectAtIndex:[indexPath section]];
NSString *sectionContent = [self.tableData objectForKey:sectionHeader];
return sectionContent;
}
And this is to get the size of the text.
- (CGSize)getSizeOfText:(NSString *)text withFont:(UIFont *)font
{
return [text sizeWithFont:font constrainedToSize:CGSizeMake(280, 500)];
}
Just a thought:
What if you had, say, six different types of cells each with their own identifier and a fixed height. One would be for a single-line cell, the other for a two-line cell, etc...
Every time your model changes, calculate the height for that row then find the nearest cell type that has height closest to what you need. Save that celltype identifier with the model. You can also store the fixed row height for that cell in the model so you can return it in the tableview:heightForRowAtIndexPath call (I wouldn't get too hung up on forcing it to calculate inside the cell class itself--technically it's not part of the cell drawing functionality and more something the tableview uses to decide which cell type to create).
At runtime, when asked to return a cell for that row all you need to do is create (or obtain from the cell cache) a cell with the celltype identifier, load the values and you're good to go.
If the cell height calculation is too slow, then you could pull the same trick the tableview cache does and do it only on-demand when the cell comes into view. At any given time, you would only have to do it for the cells in view, and then only for a single cell as it scrolls into view at either end.
I realise this won't work for you due to the infinite loop you mention, but I've had some success with calling the cells layoutSubViews method
Though this may be a little inefficient due to multiple calls to both cellForRowAtIndexPath and layoutSubViews, I find the code is cleaner.
-(float)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
MyCell *cell = (MyCell *)[self tableView:tableView cellForRowAtIndexPath:indexPath];
[cell layoutSubviews];
return CGRectGetHeight(cell.frame);
}
And in the layout code:
- (void)layoutSubviews
{
[super layoutSubviews];
//First expand the label to a large height to so sizeToFit isn't constrained
[self.myArbitrarilyLengthLabel setFrame:CGRectMake(self.myArbitrarilyLengthLabel.frame.origin.x,
self.myArbitrarilyLengthLabel.frame.origin.y,
self.myArbitrarilyLengthLabel.frame.size.width,
1000)];
//let sizeToFit do its magic
[self.myArbitrarilyLengthLabel sizeToFit];
//resize the cell to encompass the newly expanded label
[self setFrame:CGRectMake(self.frame.origin.x,
self.frame.origin.y,
self.frame.size.width,
CGRectGetMaxY(self.myArbitrarilyLengthLabel.frame) + 10)];
}