I want to center a subclassed tableview (TSNInformationTableView) in its superview using xib file.
The height of the table is set using a custom intrinsic size:
The issue is the table can have a dynamic size/height, different number of cells with different text inside of them. So in the TSNInformationTableView I have defined the intrinsicContentSize method:
- (CGSize) intrinsicContentSize {
return self.contentSize
}
The problem I have with the self.contentSize.height is it does not return the correct height of the table but something somewhat smaller. That is why I tried to compensate this with the multiplier 1.45. It does not scale properly with different number od the cells.
In the image there is a visible cut of the last cell because the height of the table defined by the intrinsicContentSize is not correct.
The table is initialized with the following code:
self.informationTableView.estimatedRowHeight = 80;
self.informationTableView.rowHeight = UITableViewAutomaticDimension;
[self.informationTableView setScrollEnabled:NO];
UPDATE
I had to add this method in the controller where the table view (TSNInformationTableView) is nested:
- (void) viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
if(!self.isInformationTableViewLoaded) {
self.isInformationTableViewLoaded = YES;
[self.InformationTableView invalidateIntrinsicContentSize];
[self.InformationTableView setNeedsLayout];
}
}
isInformationTableViewLoaded is just a simple BOOL property in the controller indicating that the table has been created (so that we can get proper table view size). It works without any animation issue now.
Also the table's estimatedRowHeight should be set to e.g. 1000.
I had to add - (void) viewDidLayoutSubviews in the controller where the table is nested. See the edit at the end of the original post.
Related
I have a table view with a table header view created through interface builder inside the same xib. I want to set the height of the header based on the screen size, for example 50% of the screen height.
Here is what I get with a static height on an iPhone 4s:
And here is is what I get on an iPhone 6:
Note that the content of the header is static.
I cannot set constraints to the header using auto layout. I tried to set a height constraint based on the height of the table view but it does not seem to be possible in interface builder. Control-dragging does not work. I cannot drag a line from the header to the table view or even to the header itself.
How can I set the header's height based on the screen size?
Unfortunately, table header views cannot be sized using auto layout. You can use auto layout for elements inside the header but you have to specify the header's size by explicitly setting its frame. If the header's height is static and known at compile time you can use IB. However, if the height is dynamic or depends on the device (as in your case), you have to set it in code.
A quite flexible solution would be to create a custom subclass of UITableView and adapt the header's frame in the layoutSubviews method. This way the header's size gets automatically adjusted when the table view is resized. You have to be careful, however, to only re-apply the header's frame when a change is actually needed to avoid an infinite loop.
Here's what it would look like in Objective-C:
#interface MyTableView : UITableView
#end
#implementation MyTableView : UITableView
- (void)layoutSubviews {
[super layoutSubviews];
if (self.tableHeaderView) {
UIView *header = self.tableHeaderView;
CGRect rect = CGRectMake(0, 0, self.bounds.size.width,
self.bounds.size.height / 2);
// Only adjust frame if needed to avoid infinite loop
if (!CGRectEqualToRect(self.tableHeaderView.frame, rect)) {
header.frame = rect;
// This will apply the new header size and trigger another
// call of layoutSubviews
self.tableHeaderView = header;
}
}
}
#end
The Swift version looks like this:
class MyTableView: UITableView {
override func layoutSubviews() {
super.layoutSubviews()
if let header = tableHeaderView {
let rect = CGRectMake(0, 0, bounds.size.width, bounds.size.height / 2)
// Only adjust frame if needed to avoid infinite loop
if !CGRectEqualToRect(header.frame, rect) {
header.frame = rect
// This will apply the new header size and trigger
// another call of layoutSubviews
tableHeaderView = header
}
}
}
}
Note that the above snippets use the bounds of the table view rather than the screen size to calculate the header size.
Update: Note that sometimes an additional call to layoutIfNeeded is needed after setting the tableHeaderView property. I ran into an issue where section headers were drawn above the header view without calling layoutIfNeeded.
I have tried the following code and it seems to work on iOS7 and iOS8. It changes the height of the header frame to half the screen height. You might want to subtract the height of the navigation and status bar from the screen height before /2, if the header has to be half the size of the table view area only.
- (void)viewDidLoad
{
[super viewDidLoad];
// Your other code
// Set the table header height
CGRect headerFrame = self.tableView.tableHeaderView.frame;
headerFrame.size.height = [[UIScreen mainScreen] bounds].size.height/2;
self.tableView.tableHeaderView.frame=headerFrame;
}
Using Storyboards and Autolayout, I have a UIViewController with a UIScrollView as the main view. I have several container views embedded in the scroll view. Some of those embedded container views contain UITableViews, each having cells of different heights. I'll need the tableView's height to be large enough to show all cells at once, as scrolling will be disabled on the tableView.
In the main UIViewController, container view's height has to be defined in order for the scroll view to work properly. This is problematic because there's no way for me to know how large my tableView will be once all it's cells of varying heights are finished rendering. How can I adjust my container view's height at runtime to fit my non-scrolling UITableView?
So far, I've done the following:
// in embedded UITableViewController
//
- (void)viewDidLoad {
// force layout early so I can determine my table's height
[self.tableView layoutIfNeeded];
if (self.detailsDelegate) {
[self.detailsTableDelegate didDetermineHeightForDetailsTableView:self.tableView];
}
}
// in my main UIViewController
// I have an IBOutlet to a height constraint set up on my container view
// this initial height constraint is just temporary, and will be overridden
// once this delegate method is called
- (void)didDetermineHeightForDetailsTableView:(UITableView *)tableView
{
self.detailsContainerHeightConstraint.constant = tableView.contentSize.height;
}
This is working fine and I was pleased with the results. However, I have one or two more container views to add, which will have non-scrolling tableViews, and I'd hate to have to create a new delegate protocol for each container view. I don't think I can make the protocol I have generic.
Any ideas?
Here's what I ended up doing:
// In my embedded UITableViewController:
- (void)viewDidLoad
{
[super viewDidLoad];
self.tableView.rowHeight = UITableViewAutomaticDimension;
self.tableView.estimatedRowHeight = 60.0;
// via storyboards, this viewController has been embeded in a containerView, which is
// in a scrollView, which demands a height constraint. some rows from our static tableView
// might not display (for lack of data), so we need to send our table's height. we'll force
// layout early so we can get our size, and then pass it up to our delegate so it can set
// the containerView's heightConstraint.
[self.tableView layoutIfNeeded];
self.sizeForEmbeddingInContainerView = self.tableView.contentSize;
}
// in another embedded view controller:
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
self.sizeForEmbeddingInContainerView = self.tableView.contentSize;
}
// then, in the parent view controller, I do this:
// 1) ensure each container view in the storyboard has an outlet to a height constraint
// 2) add this:
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
self.placeDetailsContainerHeightConstraint.constant = self.placeDetailsTableViewController.sizeForEmbeddingInContainerView.height;
self.secondaryExperiencesContainerHeightConstraint.constant = self.secondaryExperiencesViewController.sizeForEmbeddingInContainerView.height;
}
I haven't done this yet, but it'd probably be best to create a Protocol with a property of CGSize sizeForEmbeddingInContainerView that each child view controller can adopt.
Here's what worked for me perfectly.
- (void)updateSizeBasedOnChildViews {
// Set height of container to match embedded tableview
CGRect containerFrame = self.cardTableContainer.frame;
containerFrame.size.height = [[[self.cardTableContainer subviews] lastObject]contentSize].height;
self.cardTableContainer.frame = containerFrame;
// Set content height of scrollview according to container
CGRect scrollFrame = self.cardTabScrollView.frame;
scrollFrame.size.height = containerFrame.origin.y + containerFrame.size.height;
// + height of any other subviews below the container
self.cardTabScrollView.contentSize = scrollFrame.size;
}
Just like in the native iOS Mail app, when I push a UITableViewController onto a UINavigationController, I would like to make it so that the UITableView initially appears slightly scrolled downwards, obscuring its headerView beneath the navigation controller's navigation bar.
At the same time, even if the height of all of the cells is smaller than the height of the table view, it should be possible for the user to scroll up and down to explicitly show or hide the header view again.
With that logic, it would appear that there are two considerations to make for this implementation:
1) Ensuring that the minimum content size of the table view is at least the height of the table view's frame + the height of the header view.
2) When the table view is initially presented, the content offset is incremented by the height of the header view.
I've tried manually setting both the contentOffset and contentSize properties of the table view in 'viewWillAppear', however this appears to have no effect (It's possible the table view is getting reloaded after that point). Trying to set them in 'viewDidAppear' will work, but that's too late as it only gets called once the 'push' animation has completed.
While this sort of question has been asked before for previous iOS versions, I was unable to get any of them working in iOS 8. Additionally, they all dealt with changing the offset, but not the contentSize of the table view.
Has anyone gotten this sort of behavior working in iOS 7 and/or 8 before?
Update - (30/1/2015)
Alright. This wasn't sitting well with me last night, so I had another play with it, and I found a MUCH better and cleaner solution.
I discovered that the tableView property of UITableViewController is NOT readonly. So it actually makes more sense to simply manage the contentSize property in a UITableView subclass and then assign that subclass back to the UITableViewController.
#implementation TOCustomTableView
- (void)setContentSize:(CGSize)contentSize
{
CGFloat scrollInset = self.contentInset.top + self.contentInset.bottom;
CGFloat height = (CGRectGetHeight(self.bounds) - scrollInset) + CGRectGetHeight(self.tableHeaderView.frame);
contentSize.height = MAX(height, contentSize.height);
[super setContentSize:contentSize];
}
#end
---
#implementation TOCustomTableViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.tableView = [[TOCustomTableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
}
#end
This way, the table view's minimum contentSize is always explicitly set to be the height of the table view + the headerView size, achieving the desired effect with zero jittering. :)
Original Answer
trick14 pointed me in the right direction. So the correctly functioning code I ended up with.
- (void)resetTableViewInitialOffset
{
CGPoint contentOffset = self.tableView.contentOffset;
contentOffset.y = self.tableView.contentInset.top + CGRectGetHeight(self.headerView.frame);
self.tableView.contentOffset = contentOffset;
}
- (void)resetTableViewContentSize
{
CGSize contentSize = self.tableView.contentSize;
CGFloat scrollInset = self.tableView.contentInset.top + self.tableView.contentInset.bottom;
CGFloat height = (CGRectGetHeight(self.view.bounds) - scrollInset) + CGRectGetHeight(self.headerView.frame);
contentSize.height = MAX(height, contentSize.height);
self.tableView.contentSize = contentSize;
}
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
if (!self.headerBarInitiallyHidden) {
[self resetTableViewContentSize];
[self resetTableViewInitialOffset];
self.headerBarInitiallyHidden = YES;
}
}
I'm also making sure to call 'resetTableViewContentSize' each time I perform a 'reloadData' on the table view as well.
I am now followind maybe a bit obsolete tutorial from iOS Apprentice regarding geolocation.
Anyway the job to be done seems extremly easy:
placing text view inside table view cell. On storyboard everything looks greate but when I run it, it looks like presented on picture below (the textview covers the below category table cell view item):
I have following settings:
What is the best way to keep textview inside the table view cell and text just wraps, and overllay-y (so it is called in CSS - I am newbie to iOS) will be added to textview?
I agree with #railwayparade you should use
cell.textLabel.numberOfLines = 0;
Use heightForRowAtIndexPath to calculate the size the cell needs to be
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
// replace "THE_TEXT" with the text you are displaying and "TEXT_LABEL_FONT" with the font you're using in the cell
CGSize size = ["THE_TEXT" sizeWithFont:"TEXT_LABEL_FONT" constrainedToSize:CGSizeMake(self.table.bounds.size.width, HUGE_VALF) lineBreakMode:NSLineBreakByWordWrapping];
return size.height;
}
You might want to subclass UITableViewCell and override layoutSubviews to set the textLabel to the exact right size, and add padding as you see fit
-(void)layoutSubviews // in your UITableViewCell subclass
{
[super layoutSubviews];
self.textLabel.frame = CGRectMake(0.0f,0.0f,self.contentView.bounds.size.width,self.contentView.bounds.size.height);
}
Don't forget if you do add padding on the width in layout subviews, you'll have to change the constrained width in heightForRowAtIndexPath
I have a question about the usage of UITableView. I have added a UIView above the cells of my UITableView (see image).
This is very nice because I can add some images and labels there and it will scroll with the cells of the table view. Now I am calling some REST API to get an image which I want to add in this view above the cells. The problem now is that I dont know the height of the image, so I have to calculate it based on the aspect ratio which already works fine. When I add the image I can change its height correctly and move down labels and buttons BUT the image overlaps some of the visible cells.
My question: How can I move down the frame of the container? of the cells? dynamically based on my image respective View height?
I have tried to set the height of the View in the TableView but it has no effect. So I suppose that I have to set the y start position of the cells but I dont know how.
Do I need to set an y offset in the delegate method -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath ?
Any ideas?
I think the key to this is setting your view to be the table view's tableHeaderView after you change the size of the view. I did it like this in a test app,
-(void)layoutHeader {
self.label.text = #"This is a long text to see if it expands to take up multple lines. The quick red fox jumped over the lazy brown dog.";
[self.label setPreferredMaxLayoutWidth:self.tableView.frame.size.width];
CGRect stringRect = [self.label.text boundingRectWithSize:CGSizeMake(self.tableView.bounds.size.width - 40,CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin attributes:#{NSFontAttributeName:self.label.font} context:nil];
CGRect headerFrame = self.header.frame;
headerFrame.size.height = stringRect.size.height + 40;
self.header.frame = headerFrame;
[self.tableView beginUpdates];
self.tableView.tableHeaderView = self.header;
[self.tableView endUpdates];
}
I called this with a delay from viewDidLoad as a test. The beginUpdates, endUpdates code isn't necessary if you don't want to see the rows move down to accommodate the new view size. The property, header, is an IBOutlet to the view I added to the top of the table view in IB, and "label" is a subview of that view.
I would personally just use tableView:viewForHeaderInSection: to build the view out, and in tableView:heightForHeaderInSection: calculate the new height and return that. That way you don't have to worry about bumping things down within the tableView since UITableView will handle the rest for you once you. Just make sure to call [_tableView reloadData]; on your tableView after you get the image.