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:
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!
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:
Add a headerView to a UITableView.
Add a subview to headerView, let's call it wrapper.
Make wrapper's height be adjusted with it's subviews (via Auto
Layout).
When autolayout had finished layout, set headerView's height to
wrapper's height. (see -updateTableViewHeaderViewHeight)
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.
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.
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.
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.
Related
is there any possible way to set a UIImageView as a background for a section (let's say section #3) in a grouped UITableView?
I'm not asking about viewForHeaderInSection 'coz I tried it but didn't work as expected.
thanks so much in advance ...
My guess is that if you were to think very creatively, you could come up with a way to essentially swap the UITableView's backgroundView property as different sections are visible. This doesn't strike me as particularly elegant given that multiple sections might occupy the visible screen at one time, but perhaps I don't understand the question clearly.
The short answer is that there is not a defined/simple way to achieve this. The composition of a section is far removed from the background of a UITableView, and essentially the architecture isn't setup for what you want. I wouldn't assume to know the details of your implementation, but I would also urge caution: The visual and architectural characteristics of UITableView's are pretty well-considered. My personal opinion (again, I haven't seen what you're working on), is that different section backgrounds might overwhelm the user experience in many cases. I can also see cases where it may be a nice UI touch if executed properly.
In this case, if you still want to do it, here is the approach I'd take:
Essentially you're going to watch the position of your tableview's cells. You could do this in scrollViewDidScroll, or tableViewWillDisplay cell - or other places, I'm sure. But you need to know which cells are scroll on and off the screen, and you then need to be able to ask the upper-most-visible cell what it's section is. Once you've established which section it is you should currently be displaying, you can use that to scroll your own set of views representing each section's background.
Essentially, you're going to create each of your dynamic section background views in code, just UIView's, each with it's backgroundProperty set to a repeating pattern (obviously, heights will be dynamic). Add all your section background subviews (or preferably do it lazily) to your UITableView's backgroundView.
Now, as your scroll view scrolls, you're going to observe which rows and sections are coming in and out of the table's view. As rows in sections are scrolled, calculate the height of the section background (multiply the quantity of rows in the section by their heights) and adjust it's Y axis, which is sitting in but clipped by your tableView's backgroundView. As sections scroll on, you'll update the Y offset of the relevant section's background view. Phew!
Another idea might be to toss all your section background views, laid out vertically, in a UIScrollView, user interaction disabled. Place that scroll view in your table view's backgroundView, and then figure out the math to essentially "forward" scroll events from the tableview's scrolling view to your background scrolling view.
This will probably take a bit of work to find an implementation that keeps your animations all smooth and in sync, but, it's an approach that I think could be made to work.
Use myTable.tableHeaderView = customHeaderView if your table has only one section.
viewForHeaderInSection might not have worked if you didn't return an object of type UIView or a subclass in your method. Add some code from your viewForHeaderInSection method to your question for a more precise answer.
EDIT- If by viewForHeaderInSection didn't work as expected, you mean that the header was clipped, then it might be because you have not have implemented the following method:
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
If not implemented, each section's height is set to an iOS default value.
create a custom headerView that looks something like this.
.h
#interface CustomViewForHeader: UIView
#end
.m
#import "CustomViewForHeader.h"
#implementation CustomViewForHeader
-(id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self != nil) {
UIImageView *bgImage = [[UIImageView alloc]initWithFrame:self.frame];
bgImage.image = [UIImage imageNamed:#"bill_headerBg.png"];
[self addSubview:bgImage];
}
return self;
}
#end
use it like:
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
return 50.0;
}
-(UIView*)tableView:(UITableView*)tableView viewForHeaderInSection:(NSInteger)section {
CustomViewForHeader *customView = [[CustomView alloc]initWithFrame:CGRectMake(0,0,320,50)]
return customView;
}
I recall something about methods to find the rectangles of each section. Having those, you could appropriate sized images to the scroll view underlying the table view.
Aside from that, you could put the background image into the background of the cells themselves. When the tableview calls for a cell in a given section, you can pick the appropriate image or image tile.
Is there any way to decrease the standard width of grouped UITableViewCell and put a custom button on the left side(outside of cell boundary)? I tried to change the cell size but it keeps same
You are going to have to fake the editing mode.
What I mean by that is that as AtomRiot said you have to subclass UITableViewCell so that when in editing mode you show the button you want on the left, outside the cell.
But first things first.
To change the indentation level for your cells all you need to do is implement this delegate method for the UITableView
- (NSInteger)tableView:(UITableView *)tableView indentationLevelForRowAtIndexPath:(NSIndexPath *)indexPath
So that takes care of it. Then in your UITableViewCell subclass all I would do is to implement the method
- (void)setEditing:(BOOL)editing animated:(BOOL)animated {
which I assume is called when the table the cell belongs to has changed to editing mode.
There I would fade in (or animate in any way you want) a button to appear on the left of your cell.
I have done it inside a grouped-style cell but never on the outside.
Give it a try!
You could subclass UITableCell and add your own custom views inside of it. I have not personally added a button inside one but it should work. It may get confused with the row selected call the tableview makes if you are implementing that.
The Cocoanetics blog seems to have a pretty good solution to this:
http://www.cocoanetics.com/2010/03/how-to-shrink-cells/
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.
I have several parts in my app where I use custom table view cells.
Their content is created with subviews.
The problem is that on some of these cells, the content does not appear at all or does not appear correctly until after the cell was selected for the first time.
One example is a custom cell which has a custom subview which can be set after its creation. This view does not appear at all before I selected the cell and its views were redrawn. Calling -[setNeedsDisplay] in the subview's setter method does not help either.
The problems was that I was using the cells themselves to calculate their height. For some reason, the subviews (which were part of the cell used to calculate the height) weren't appearing correctly in the cells that were used for the actual displaying.
Therefore my advice: Never use a UITableViewCell to calculate its own height. This may work in principle (it doesn't crash), but might bite you later in unexptected and hard-to-debug ways.
I have a UIView (created in IB) with a grouped UITableView as a subview. Below this table view is a UIButton. The XIB containing the view will be loaded by a few different viewcontrollers, and so the contents of the table view can vary between one and four cells.
Here's what I want to achieve: when the view loads, the height of the tableview (tableView.frame.size.height) should be adjusted depending on the number of cells, and the button should be placed just beneath the table view.
Can this be done? Could it somehow be done if the view is created programmatically?
Thanks in advance
Edit: Pxl's suggestion was just what I was looking for. A while later, the need arose to have more than just a button below the table view - this was accomplished by creating a separate view containing everything I needed, and implementing the tableView:viewForFooterInSection: and tableView:heightForFooterInSection: functions.
A note for those of you trying to do the same thing: the tableview has to be programmatically created if you want different heights for the footers, or footers for only some of the sections. This is because the footer height set in IB will override the one returned from the tableView:heightForFooterInSection: function.
if there are only a handful of rows, may i suggest that you create a special UITableViewCell that contains just a button?
then make that button cell the bottom row of the last group all the time. make the group so that it will be unlabeled and appear as if the button is sitting at the bottom of your tableview. this way you won't have to muck around with recalculating the tableview's frame and redrawing it.
if the tableview will scroll due to there being many rows, then you'd be calculating the height of the tableview up to a set max (at which point the tableview will need to scroll to show more rows).
once you've determined the height of the tableview you'll need to display your rows, make a frame of the appropriate size, set the tableview's frame to it, position the button just under the tableview, and then redraw the view.
the layout and positioning in this case will need to be done programmatically.
UITableview is a subclass of UIView, so you can change its frame to suit your needs just like a UIView, and UITableView will manage drawing itself to whatever frame you give it.
Just use the methods UITableViewDataSource and UITableViewDelegate provides you.
height = [self tableView:numberOfRowsInSection]*[self tableView:heightForRowAtIndexPath:] + A_CONSTANT_FOR_HEADER_AND_FOOTER_HEIGHT
I agree with pxl that adding a cell with the button in it may be the easiest way to accomplish what you want.
Whether or not you do that, the table view's sizeToFit method should resize the view to (just) fit its contents. If that doesn't work, you can use numberOfSections and rectForSection: to build a loop that determines the height of the table's contents, and reset its frame manually. (And then position the button underneath.)