I need to get the full height of a UITableView (i.e. the height at which there would be nothing more to scroll). Is there any way to do this?
I've tried [tableView sizeThatFits:CGSizeZero], but that only returns a 0x0 CGSize.
Try the contentSize method, which is inherited from UITableView’s superclass, UIScrollView. However, you may find that contentSize returns an incorrect or out of date value, so you should probably call layoutIfNeeded first to recalculate the table’s layout.
- (CGFloat)tableViewHeight
{
[tableView layoutIfNeeded];
return [tableView contentSize].height;
}
Obligatory Swift 3.0 & 2.2 answer.
var tableViewHeight: CGFloat {
tableView.layoutIfNeeded()
return tableView.contentSize.height
}
Try passing in a different CGSize parameter instead of CGSizeZero. The sizeThatFits: method uses that parameter to calculate its result. Try passing in self.view.size from whatever class is making that call.
If a table view rows count changed and you indeed need to know the content size of table view incorporating the last changes, I didn't find that layoutIfNeeded method actually helps.
After a little bit hacking, I get to know how to force table view recalculate its content size. In my case, it is enough to reset table view frame to get it working:
- (CGSize)com_lohika_contentSize
{
CGRect theFrame = self.frame;
self.frame = CGRectZero;
self.frame = theFrame;
[self layoutIfNeeded];
return [self contentSize];
}
Related
I am working on a view which inherits from UIScrollView, and the requirement is that it should start at a contentOffset.y position that is dependent on the view size. Specifically I want to start one screen down in a content that is 3 x the view height.
Like this:
- (void)configureStartCondition {
self.contentSize = CGSizeMake(self.bounds.size.width, self.bounds.size.height * 3.0);
self.contentOffset = CGPointMake(0.0, self.bounds.size.height * 1.0);
}
The view itself is wired up with constraints in Storyboard, just like any view. As it works, the framework will initially give the view the size it has in the storyboard, then when the device size is known, the view's size will be changed to its final size. This is how it should work, and I am fine with this. My question is where do I call configureStartCondition?
An obvious solution would be to put this code in setFrame:, but it doesn't work. setFrame: is only called for the initial frame size, which might or might not be the final size. Why is this?
// NOT working
- (void)setFrame:(CGRect)frame {
[super setFrame:frame];
[self configureStartCondition];
}
A more common place would be in layoutSubview, where I usually do this kind of setup. However, as it is a UIScrollView the layoutSubview is called very frequently as the user scrolls the view. Meaning I would need to save the last height and compare it to make things work, then run through this test millions of times just to be able to initialize. It feels like a kludge to me.
// Working, but ugly
- (void)layoutSubview {
[super layoutSubviews];
if (self.bounds.size.height != self.savedHeight) {
self.savedHeight = self.bounds.size.height;
[self configureStartCondition];
}
// Do layout stuff
}
Another place that may seem good is setBounds:. It will get called for the view size change, but since the contentOffset property is tied to the bounds property, I actually get as many calls here as to layoutSubviews.
So, is there a better place to do it, or a better way to do it?
Side issue, less important in my case, but can the content offset be set from a storyboard?
EDIT: Solutions in Swift are also fine.
setContentSize: works perfect for me. It called only when size changed.
PS: My code to check: (sorry for swift but UIKit make no difference)
class CustomScrollView: UIScrollView {
override var contentSize: CGSize{
didSet{
var offset = contentOffset
offset.y = contentSize.height / 2.0
contentOffset = offset
}
}
}
I've been searching for this for many hours but didn't find a way to make it.
I have a UITableView whose UITableViewCells use AutomaticDimension for Height:
tableView.RowHeight = UITableView.AutomaticDimension;
tableView.EstimatedRowHeight = 160f;
The problem is when I try to get tableView's ContentSize. It seems to be calculated based on EstimatedRowHeight instead of the current height of its rows. Suppose, if there are 10 Cells then ContentSize's returned value is 160x10.
Then my question is if there is a way to do this.
Any help will be really appreciated... I use Xamarin.iOS but Obj-C and Swift answers are obviously welcome :)
I think #Daniel J's answer only works because of the way the animation completion handler is scheduled - on the face of it, there is no guarantee the table reload will have happened before the animation completes.
I think a better solution is (in Swift, as that is what I was using):-
func reloadAndResizeTable() {
self.tableView.reloadData()
dispatch_async(dispatch_get_main_queue()) {
self.tableHeightConstraint.constant = self.tableView.contentSize.height
UIView.animateWithDuration(0.4) {
self.view.layoutIfNeeded()
}
}
}
I actually used the animation delay because of the effect I wanted, but it does also work with a duration of zero. I call this function every time I update the table contents and it animates in and resizes as necessary.
I had a scenario where I needed to know the contentSize as well, to set the frame on a non-fullscreen tableView that had varying cell counts of varying heights. Then I'd adjust the tableView (via NSLayoutConstraint) to be the height of that content.
The following worked, though I feel it's a bit hacky:
Setup all code in 'viewDidLoad'
Set the rowHeight to automatic
Set the estimatedRowHeight to larger than I expected a cell to ever be (in this case, I didn't expect a cell to ever be taller than about 60pts, so set the estimated to 120)
Wrap the tableView reload in an animation block, and then query the contentSize and take appropriate action in the completion block
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.rowHeight = UITableViewAutomaticDimension;
self.tableView.estimatedRowHeight = 120.0;
[UIView animateWithDuration:0 animations:^{
[self.tableView reloadData];
} completion:^(BOOL finished) {
self.tableViewHeight.constant = self.tableView.contentSize.height;
[self.view layoutIfNeeded];
}];
}
self.coins = coins
tableView.reloadData()
view.layoutIfNeeded()
tableViewHeightConstraint.constant = tableView.contentSize.height
view.layoutIfNeeded()
worked without UITableViewAutomaticDimension and estimatedRowHeight. iOS 11
tableView.reloadData()
view.layoutIfNeeded()
tableViewHeightConstraint.constant = tableView.contentSize.height
view.layoutIfNeeded()
Before getting tablaview.contentSize.height
reload tableView
You need to update the view.
When reloadData gets called, it seems that the UITableView recalculate its contentSize automatically so that the content will fit (see screenshot of the call stack in Xcode). How do I stop that?
I want to have the contentSize to be bigger than its content in some cases, when the table is partly obscured. But any changes of the contentSize will disappear after reloading.
You need to change the contentSize of UITableView after every "reloadData" call, you can do this :
[self.tableView reloadData];
self.tableView.contentSize = CGSizeMake(self.tableView.contentSize.width, self.tableView.contentSize.height < 44 ? 150/*any desired value you like*/ :self.tableView.contentSize.height);
I solved it by using contentInset instead, like this:
float extraSpaceAtTheBottom = 50;
self.tableView.contentInset = UIEdgeInsetsMake(0.0, 0.0, extraSpaceAtTheBottom, 0.0);
Set a layout constraint for height on the tableView.
Inside a UICollectionView's supplementary view (header), I have a multiline label that I want to truncate to 3 lines.
When the user taps anywhere on the header (supplementary) view, I want to switch the UILabel to 0 lines so all text displays, and grow the collectionView's supplementary view's height accordingly (preferably animated). Here's what happens after you tap the header:
Here's my code so far:
// MyHeaderReusableView.m
// my gesture recognizer's action
- (IBAction)onHeaderTap:(UITapGestureRecognizer *)sender
{
self.listIntro.numberOfLines = 0;
// force -layoutSubviews to run again
[self setNeedsLayout];
[self layoutIfNeeded];
}
- (void)layoutSubviews
{
[super layoutSubviews];
self.listTitle.preferredMaxLayoutWidth = self.listTitle.frame.size.width;
self.listIntro.preferredMaxLayoutWidth = self.listIntro.frame.size.width;
[self layoutIfNeeded];
CGFloat height = [self systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
self.frame = ({
CGRect headerFrame = self.frame;
headerFrame.size.height = height;
headerFrame;
});
NSLog(#"height: %#", #(height));
}
When I log height at the end of layoutSubviews, its value is 149 while the label is truncated and numberOfLines is set to 3. After tapping the headerView, setting numberOfLines to 0, and forcing a layout pass, height then gets recorded as 163.5. Great!
The only problem is that the entire headerView doesn't grow, and the cells don't get pushed down.
How can I dynamically change the height of my collectionView's supplementary view (preferably animated)?
I'm aware of UICollectionViewFlowLayout's headerReferenceSize and collectionView:layout:referenceSizeForHeaderInSection: but not quite sure how I'd use them in this situation.
I got something working, but I'll admit, it feels kludgy. I feel like this could be accomplished with the standard CollectionView (and associated elements) API + hooking into standard layout/display invalidation, but I just couldn't get it working.
The only thing that would resize my headerView was setting my collection view's flow layout's headerReferenceSize. Unfortunately, I can't access my collection view or it's flow layout from my instance of UICollectionReusableView, so I had to create a delegate method to pass the correct height back.
Here's what I have now:
// in MyHeaderReusableView.m
//
// my UITapGestureRecognizer's action
- (IBAction)onHeaderTap:(UITapGestureRecognizer *)sender
{
self.listIntro.numberOfLines = 0;
}
- (void)layoutSubviews
{
[super layoutSubviews];
self.listTitle.preferredMaxLayoutWidth = self.listTitle.frame.size.width;
self.listIntro.preferredMaxLayoutWidth = self.listIntro.frame.size.width;
CGFloat height = [self systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
self.frame = ({
CGRect headerFrame = self.frame;
headerFrame.size.height = height;
headerFrame;
});
if (self.resizeDelegate) {
[self.resizeDelegate wanderlistDetailHeaderDidResize:self.frame.size];
}
}
// in my viewController subclass which owns the UICollectionView:
- (void)wanderlistDetailHeaderDidResize:(CGSize)newSize
{
UICollectionViewFlowLayout *flowLayout = (UICollectionViewFlowLayout *)self.collectionView.collectionViewLayout;
// this is the key line
flowLayout.headerReferenceSize = newSize;
// this doesn't look beautiful but it's the best i can do for now. I would love for just the bottom of the frame to animate down, but instead, all the contents in the header (the top labels) have a crossfade effect applied.
[UIView animateWithDuration:0.3 animations:^{
[self.collectionView layoutIfNeeded];
}];
}
Like I said, not the solution I was looking for, but a working solution nonetheless.
I ran into the same issue than you, so I was just wondering: did you ever get a solution without the crossfade effect that you mention in the code sample?. My approach was pretty much the same, so I get the same problem. One additional comment though: I managed to implement the solution without the need for delegation: What I did was from "MyHeaderReusableView.m" You can reference the UICollectionView (and therefore, the UICollectionViewLayout) by:
//from MyHeaderReusableView.m
if ([self.superview isKindOfClass:UICollectionView.class]) {
//get collectionView reference
UICollectionView * collectionView = (UICollectionView*)self.superview;
//layout
UICollectionViewFlowLayout * layout = (UICollectionViewFlowLayout *)collectionView.collectionViewLayout;
//... perform the header size change
}
A number of S.O. questions show an autolayout technique to determine the minimum size required by a view requires to fulfil its constraints: [header systemLayoutSizeFittingSize: UILayoutFittingCompressedSize]
Before making the systemLayoutSizeFittingSize: call, all the examples I've seen force a layout update, like this:
[view setNeedsLayout];
[view layoutIfNeeded];
CGFloat height = [view systemLayoutSizeFittingSize: UILayoutFittingCompressedSize].height;
I'd like to know when this is actually necessary as it seems sprinkled in as a ritual seasoning: I'd like to understand why I'm making calls, rather than doing it for luck!
I've just used systemLayoutSizeFittingSize: in some code where I selectively update a view that's a UITableView instance's tableViewHeader (not section header), then resize it. It seems to work fine without the extra calls. I have this in my viewDidLoad:
{
// Remove the view that we don't want.
[self.autoPopulateView removeFromSuperview];
// Resize the table's header view now the unwanted view is removed.
UIView *const header = self.tableView.tableHeaderView;
// Don't explicitly layout.
// [header setNeedsLayout];
// [header layoutIfNeeded];
CGFloat height = [header systemLayoutSizeFittingSize: UILayoutFittingCompressedSize].height;
CGRect frame = header.frame;
frame.size.height = height;
header.frame = frame;
}
Thanks.
There definitely shouldn't be a need to make either of those calls prior to calling systemLayoutSizeFittingSize. As long as all your constraints are in place, you shouldn't need to do anything else.
In fact, forcing a layout pass beforehand is potentially detrimental from a performance standpoint, and I would argue that doing so is not only unnecessary, but actually harmful.
it definately seems like layoutIfNeeded is a catch-all response to seeing inconsistencies of height calculations when using systemLayoutSizeFittingSize - the problem is that the value is very difficult to debug when it's incorrect
In my experience you need to call layoutIfNeeded whenever your target view has updated the constraints in code, for example changing a constant, adding, removing a constraint. Calling setNeedsLayout or setNeedsUpdateConstraints does not work (shrug)