So I want to lay out a UITableView and I want the cells to calculate their own height. I can do this with systemSizeFittingSize, but this doesn't always return the correct height, as the rows still have their frame from the storyboard (or xib).
I want to update the frames so that they are what they will be on the device.
How can I do this? I have tried setNeedsLayout and layoutIfNeeded, but I would have to do this on the highest view in the hierarchy to achieve the desired effect, as each view seems to use the frame of their superview.
I am setting the tableViewDataSource and calling reloadData in viewDidLoad in my viewcontroller.
Is there a way to set the correct frames before viewDidLoad gets called?
Edit: I want to give the height of a subview to a layout object in my viewDidLoad, so that it can work its magic providing a nice layout. Now this object only does the layout calculations. I notice that the height I request (through view.frame.size.height) in viewDidLoad is the same as in my storyboard, again telling me that in viewDidLoad the frames haven't been updated yet... Anyone with a workaround?
I think tableView:heightForRowAtIndexPath: may be something that you need:
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UITableViewDelegate_Protocol/#//apple_ref/occ/intfm/UITableViewDelegate/tableView:heightForRowAtIndexPath:
But setting the correct frames before viewDidLoad is possible only if you know the size before and then you can just calculate it in init.
Related
I was reading the description of viewDidLayoutSubviews of UIViewController:
Called to notify the view controller that its view has just laid out its subviews [...] However, this method being called does not indicate that the individual layouts of the view's subviews have been adjusted. Each subview is responsible for adjusting its own layout [...].
For me, it means: "Called when the layout of subviews is complete, but actually this is not true". So what's really behind viewDidLayoutSubviews?
When bounds change for a ViewControllers View, this method is called after the positions and sizes of the subviews have changed.
So this is our chance to make changes to view after it has laid out its subviews, but before it is visible on screen.
Any changes that depending on bounds has to be done, we can do here and not in ViewDidLoad or ViewWillAppear.
While ViewDidLoad & ViewWillAppear, the frame and bounds of a view are
not finalised. So when AutoLayout has done it's job of fixing mainView and
it's subviews, this method is called.
When using autolayout, framework does not call layoutSubviews every time. This is called in these cases.
Rotating a device: only calls layoutSubview on the parent view (the responding viewControllers primary view)
Its own bounds (not frame) changed. (The bounds are considered changed only if the new value is different, including a different origin.)
A subview is added to the view or removed from the view.
Your application forces layout to occur by calling the setNeedsLayout or layoutIfNeeded method of a view.
Scrolling a UIScrollView causes layoutSubviews to be called on the scrollView, and its superview.
Note:
The call for viewDidLayoutSubviews also depends on various factors like autoresize mask, using Auto-Layout or not, and whether view is in view hierarchy or not.
For any other clarification, check When is layoutSubviews called?
viewDidLayoutSubviews will be called when
When the bounds change for a view controller's view, the view
adjusts the positions of its subviews and then the system calls this
method.
For example you have set constraints of your view then you want to update the frame for your subview in viewDidLoad(), which will not make any impact as in viewDidLoad() your constraints are not properly set, they will get properly set when viewDidLayoutSubviews get called, now you want to update the frames of your subview, then you can do that in this method as this method get called only after all the constraints of your view are properly set.
I want to programatically create UIViews which depend on the size of the bounds of self.view. I've put the code to create the UIViews in viewDidLayoutSubviews.
The problem is that viewDidLayoutSubviews is called multiple times when my viewController appears on screen, thus creating multiple instances of the UIView.
I'm thinking that this could be solved by using some sort of flag.
Is there a better way to do this? Should the code be put somewhere else in the view controller lifecycle?
You should not put creating UIView code in viewDidLayoutSubviews, you should create it in viewDidLoad instead. You can put view frame update code in viewDidLayout. Or you can use autolayout so you don't need any view update code manually. I prefer autolayout.
In the method of viewDidLayoutSubviews, you can get the updated the frame size for UIControls, after that you can programatically create UIViews in the viewDidAppear.
While I do not think you should create UIViews in viewDidLoad method, in autolayout you can not get correct size of views until viewDidLayoutSubviews has been called.
I have set up a horizontal UIScrollView in a UITableViewCell prototype in my storyboard, along with its content views. I have made use of Autolayout.
In the -cellForRowAtIndexPath: method, I am setting the contentSize as well as contentOffset for this scrollView.
The problem arises when the cells are created for the first time. Neither does the scrollView scroll, nor is the contentOffset taking place.
However, when I scroll down a few cells (when they start getting reused), the contentOffset takes place, and the scrollView is interactive.
Since I have subclassed the UITableViewCell, I have tried setting the contentOffset in the -awakeFromNib method, to no affect. I also tried doing so in the -tableView:willDisplayCell: method to no avail.
What could be going wrong? Is there a workaround?
There doesn't seem to be any relevant code that I could include. Please comment if you need more info.
EDIT
I do not know whether this is relevant, but the Storyboard is showing a warning:
Scroll View: Has ambiguous scrollable content width
The issue was because the scrollView was not being laid out after setting the contentSize. This would not have been a problem if the contentSize had been set properly in the storyBoard itself (still dont know how to do that).
Anyways, in the cellForRowAtIndexPath method, calling
[cell layoutIfNeeded];
immediately after setting the contentSize worked.
A good explanation on how this method works can be found in this thread.
I have a ViewController with a UIScrollView on it. On this VC I programatically adjust the frame of a UILabel which is inside the scroll view. This is done on viewDidLoad. This UILabel comes from the VC's xib file, it is not created programatically, only its frame changed.
When I transition from this VC to another, then back, the UILabel's frame gets reset to the XIB's state. It's text however is not reset, it stays the same text I set before.
My investigation tells me that this occur on layoutSubviews, as the UILabel's properties are correct on willLayoutSubviews, then reset on didLayoutSubviews when moving back to the VC.
Is this expected behaviour? Is there a reason why the label's text remains but the frame gets reset? Is this because the UIScrollView calls layoutSubviews on its parent view whenever scrolling?
Thanks
I see two problems in your description. First, you said this:
On this VC I programatically adjust the frame of a UILabel which is inside the scroll view. This is done on viewDidLoad.
It is generally a bad idea to modify view frames in viewDidLoad, because the system's layout phase (during which layoutSubviews messages are sent) hasn't happened yet. In viewDidLoad, your view's frame hasn't been adjusted for the current device's screen size and interface orientation yet.
Second, you said that you're using autolayout. The autolayout system sets the frames of views during the layout phase. The layout phase can be triggered by many different events, including (as you've discovered) the appearance and disappearance of views.
In order to make your adjustment to the label's frame “stick”, you need to modify the constraints that control the label's frame. One way to do this is to create an outlet of type NSLayoutConstraint on your view controller for each constraint that you need to modify, and connect these outlets to the constraints in your xib. Then in your view controller's viewWillLayoutSubviews, you can modify the constant property of each constraint as necessary. (Ironically, constant is the only modifiable property of an NSLayoutConstraint.)
I'm constructing a UITableView with variable height custom table cells, their height determined by the size of a contained multi-line UILabel. I've got the tableView:heightForRowAtIndexPath: delegate method wired up and calculating the final height correctly using sizeWithFont:constrainedToSize:.
I've run across strange issue: by the time the data source method tableView:cellForRowAtIndexPath: is called, the correct per-row height has already been determined as described above, but the frame of the cell does not match that height. Instead, the frame.size.height property of the cell is the default cell height of the table view (86 px, as I've set it in Interface Builder, the correct height when the contained UILabel has just one line of text), instead of being the height that tableView:heightForRowAtIndexPath: determined correct for that index path.
I'm producing the cells in cellForRowAtIndexPath: using dequeuing, that is,
// Using storyboards, this never returns nil, no need to check for it
CustomCell *cell = (CustomCell *)[tableView dequeueReusableCellWithIdentifier:#"SomeIdentifier"];
NSLog(#"%f", cell.frame.size.height); // 86, not correct if the cell contains a multi-line UILabel
It seems, then, that whatever iOS is doing behind the scenes, the dequeuing is not setting the frame property of the cell to match the calculated height. This in itself is not that surprising, dequeuing concerns itself with cell instances, not their geometry. The cells are rendered correctly, though, so the height property is being set somewhere, but it happens after cellForRowAtIndexPath:.
So: when I initially populate the table view, cell.frame.size.height is 86 for all the cells as they appear for the first time when I scroll the list downwards. Since the correct geometry is set sometime after the first cellForRowAtIndexPath: for each row before it's displayed, when I scroll back up, the height property is correct for each cell that comes back into view after being reused.
After this I can scroll the table view back and forth at will, and the height property remains correct for each cell from that point on.
What's the correct way of getting the correct cell height the first time around, before any dequeue-based reuse happens? I need this to do a bit of re-positioning of the subviews of the table cell. Do I need to manually call heightForRowAtIndexPath: in cellForRowAtIndexPath: and then manually set the frame of the freshly created CustomCell instance to match that height? This seems redundant, and I'd need to create a mechanism to detect when the cell is created for the first time with the wrong frame height vs. when it is dequeued with the correct frame height later to avoid this redundancy.
So, if someone can shed some light into what the logic is behind this, I'd appreciate it.
As suggested by Flexo, answering this myself is apparently better than adding an edit to the question. So, here's the previous edit as an answer:
Nevermind, I should read the docs better. I can get the correct frame in the tableView:willDisplayCell:forRowAtIndexPath: method of UITableViewDelegate, so that is the correct place to do subview customization that relies on the correct frame being set, not cellForRowAtIndexPath:.
Interesting that the docs say this, though:
After the delegate returns, the table view sets only the alpha and frame properties, and then only when animating rows as they slide in or out.
...since the correct frame is already there when this delegate method is called. But anyway, problem solved.
Don't forget that the cell is a UIView, so overriding layoutSubviews is also a valid way to get the correct frame and adjust size/position of subviews. Just don't forget to call [super layoutSubviews].
Easiest way I found was just to call cell.layoutIfNeeded() before you do any setup on the cell. This makes sure all the layout constraints are calculated and the frames are set.