10

I've been struggling with this now for a little while, and I want to make sure I'm doing this the right way. For reference, I am using AutoLayout with Storyboards, and it's an iOS7-only app.

I have a UITableView with a header-view, and the UI for the HeaderView is hooked up in the storyboard. I've left the header in the storyboard, and it contains a few elements, one of which is a non-scrollable UITextView.

Here is a screenshot of how it basically looks in the storyboard:

screenshot

I darkened the header-view's background to a darker grey so it stands out.

The UITextView's text can be dynamic, so its height needs to change depending on the size of the text. But the tricky part is that the table's header-view needs to adjust its height as well, depending on this text's size. I've tried a few different things with constraints, but it's not really working correctly (unless I disable AutoLayout and re-size things programatically, which I really would like to avoid).

I want this to be as clean as possible with as little code as necessary, of course. I am not tied to using the table-view header, although it works well with my needs, since I want it and its containing textview to scroll with the actual details below. But that all being said, if there is a more simple approach to accomplishing this (i.e. with a uilabel), I will gladly change the underlying structure.

Any thoughts as to an approach? Not looking for someone to hold my hand through an answer; just looking mainly for some good starting points if possible. Thanks in advance!

svguerin3
  • 2,433
  • 3
  • 29
  • 53
  • Describe, or ideally, insert a pic, of how you "hooked up" the HeaderView in storyboard. – bilobatum Oct 17 '13 at 20:15
  • No problem, sorry I should've done this initially. Screenshot added from the storyboard. Thanks! – svguerin3 Oct 17 '13 at 22:49
  • That was useful. What you're calling a header view isn't technically a table view header view. It's just a view that you dragged from the object library. I'll explain more in my answer. – bilobatum Oct 17 '13 at 23:38
  • Ah I didn't actually realize that; I thought it actually becomes the header when you drag it in like that (and it acts like one too, when you resize the table-view, etc). Very misleading, ha, especially since I read other StackOverflow posts indicating that's how to setup a header-view in an XIB/storyboard. Thanks for the Answer too, I'll give that a try! – svguerin3 Oct 18 '13 at 14:57

4 Answers4

33

Some time ago I found new solution, maybe it needs tweaking for animations and is experimental, but, for some range of cases it's fine, so give it a try:

  1. Add a headerView to a UITableView.
  2. Add a subview to headerView, let's call it wrapper.
  3. Make wrapper's height be adjusted with it's subviews (via Auto Layout).
  4. When autolayout had finished layout, set headerView's height to wrapper's height. (see -updateTableViewHeaderViewHeight)
  5. Re-set headerView. (see -resetTableViewHeaderView)

Here's some code:

- (void)viewDidLayoutSubviews
{
    [super viewDidLayoutSubviews];

    [self updateTableViewHeaderViewHeight];
}

/**
 tableView's tableViewHeaderView contains wrapper view, which height is evaluated
 with Auto Layout. Here I use evaluated height and update tableView's
 tableViewHeaderView's frame.

 New height for tableViewHeaderView is applied not without magic, that's why 
 I call -resetTableViewHeaderView.
 And again, this doesn't work due to some internals of UITableView, 
 so -resetTableViewHeaderView call is scheduled in the main run loop.
*/
- (void)updateTableViewHeaderViewHeight
{
    // get height of the wrapper and apply it to a header
    CGRect Frame = self.tableView.tableHeaderView.frame;
    Frame.size.height = self.tableHeaderViewWrapper.frame.size.height;
    self.tableView.tableHeaderView.frame = Frame;

    // this magic applies the above changes
    // note, that if you won't schedule this call to the next run loop iteration
    // you'll get and error
    // so, I guess that's not a clean solution and could be wrapped in @try@catch block
    [self performSelector:@selector(resetTableViewHeaderView) withObject:self afterDelay:0];
}

// yeah, guess there's something special in the setter
- (void)resetTableViewHeaderView
{
    // whew, this could be animated!
    [UIView beginAnimations:@"tableHeaderView" context:nil];

        self.tableView.tableHeaderView = self.tableView.tableHeaderView;

    [UIView commitAnimations];
}

All this works seamlessly after the initial autolayout pass. Later, if you change wrapper's contents so that it gains different height, it wont work for some reason (guess laying out UILabel requires several autolayout passes or something). I solved this with scheduling setNeedsLayout for the ViewController's view in the next run loop iteration.

I've created sample project for that: TableHeaderView+Autolayout.

MANIAK_dobrii
  • 6,014
  • 3
  • 28
  • 55
  • Thank you for sample project! I have a question - how do I put a button in the header with autolayout and make it react to clicks and animate? Right now clicks are working for me but no touch animation displayed for the button – Alex Sorokoletov Nov 25 '14 at 16:51
  • 1
    @AlexSorokoletov UITableView is a UIScrollView subclass, so it as well has option to delay touch events delivery to it's subviews. To fix this you can set delaysContentTouches to NO (or "Delays Content Touches" in IB). – MANIAK_dobrii Nov 26 '14 at 11:07
  • in my case was enought to initialize header view with frame: self.changelogView.tableView.tableHeaderView = [[SBChangelogHeaderView alloc] initWithFrame:CGRectMake(0, 0, 100, 60)]; It didn't matter what width I put, it was calculated automatically, but when I put different height, then height was different. Strange behaviour :-| ( I'm testing it on iOS 10.2 ) – Vojta Mar 09 '17 at 10:25
  • Worked like a charm..Thanks – christijk Jun 06 '17 at 07:15
  • Thanks, this worked great, although in my case I had to call `layoutIfNeeded` on the wrapper in `updateTableViewHeaderViewHeight` to assure that the measurements were correct. (Doing a header with a fixed aspect ratio & pinned to the sides of the superview.) – Matt Mc Mar 05 '18 at 05:39
0

I would suggest setting the height programatically. You can calculate the hight of the textview with boundingRectWithSize:options:context

Then you just set the frame of the tableViewHeader.

johan
  • 6,578
  • 6
  • 46
  • 68
  • I actually tried that, but it doesn't seem to affect the view sizes unless AutoLayout is disabled completely. :( I may be missing something obvious, though? – svguerin3 Oct 17 '13 at 18:49
-3

As said above, in UITableView the height for the header is not controlled by auto layout but by

-(CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section

Footer height is set by

-(CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section

And finally cell height is set by

-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{

The tableview loads the cells from storyboards, XIB files or code. It uses the above methods to set the size of the frame for the headers, footers and cells, and sizes their views to fit in that space.

It's important to understand that auto layout will not change those sizes, it only handles the rules used to layout the views inside their containers.

If your cells/headers/footers are using static sizes, then it's very easy. You just lock the heights of all your subviews, set up your cell to be the same size you return in heightFor... and everything looks how it should.

Dynamic cells/headers/footers take a bit more work.

Essentially you're asked to tell the tableview the size of your cell/header/footer BEFORE you have actually made it and sized out all the subviews.

You can quickly test this out by setting a breakpoint at the cellForRowAtIndexPath and the heightForRowAtIndexPath methods, the height is called first.

For dynamic tableviewcell heights, a common trick is to create a single cell in viewDidLoad and save it for calculating the heights. Then when you reach heightForRowAtIndexPath or heightForHeaderInSection use the pre made cell to put in the values you will be using in the final cell, for UILabels use sizeWithFont (pre iOS 7) or boundingRectWithSize (iOS 7.0+), and sizeThatFits for UITextViews. (For anybody reading this in the future, check that those methods have not been replaced by something newer)

So get the text you want into the dummy cell, get the heights of the labels and textviews, add the spaces between them and return the final size when you're done.

Then in cellForRowAtIndexPath or headerForSection redo the setting of values but on the actual cell that will show.

If your height calculations match your auto layout settings, then everything will fit perfectly in the space that's made for it.

If you want to change the height of a cell or header after it's already drawn, then you'll want to update the tableview by doing something like:

[self.tableView beginUpdates];
// change the data that will cause cell height to be different in heightForCellAtIndexPath or which will change a header or footer height calculation
[self.tableView endUpdates];

This will cause the tableview to check the sizes it has and only update the cells or header/footer if changed.

DC2
  • 119
  • 8
  • 10
    As I states in another comment above, don't confuse *the* table view header with *a* table view **section header**. Your answer refers to the later, but OP is asking about the former. – Greg Combs Mar 22 '14 at 03:11
-4

A table view header view is given to a table view via the tableView:viewForHeaderInSection: delegate method.

- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
    // create your header view here (read the docs on this method, there are some specific requirements)...

}

I don't know if it's possible to design the header view in storyboard; I guess you'd have to use an individual XIB file or do so programmatically.

In other words, I think you're headed towards creating constraints in code if you need to provide table view header views.

On the other hand, maybe you don't really need a table view header view. Maybe, what you're after is just a subview above the table view that doesn't move when the user scrolls the table. To do that, you need to re-work your view hierarchy in IB. Right now your so-called header view is inside the table view. You don't want that.

Or, since it looks like you're using static cells, you could make the top static cell take on the appearance of your so-called header view.

bilobatum
  • 8,918
  • 6
  • 36
  • 50
  • I like your latter idea of using a top static cell (since it does indeed need to scroll with the table). I actually thought of that briefly but ended up trying to force it into a header-view instead. I think the cell makes sense, since I can set it all up in the storyboard with any constraints needed. Dumb question, though - does the same concept apply to cells, with regards to my question? I.e. can I set up constraints to re-size the TextView AND the cell, based on the text, if I have it in there instead? If so, any direction there would be greatly appreciated! Thanks! – svguerin3 Oct 18 '13 at 15:01
  • I found this post, which seems to address the table-view cell dynamic height with iOS7. http://www.brynbodayle.com/faster-uitableviews-with-ios-7/ Going to try it out. Thanks again! – svguerin3 Oct 18 '13 at 16:39
  • 3
    A tableViewHeader is quite different from a tableView section header. I think OP is asking about the former, but your response is for the latter. – Greg Combs Mar 22 '14 at 03:09