I have a ViewController with a ScrollView inside (pinned to edges, 4 constraints). Everything fine.
I then add a TableViewController as a ChildViewController to the ScrollView. The TableViewController is hardcoded to have 3 rows. The TableViewController's view is pinned to the full ScrollView.
#IBOutlet weak var SV: UIScrollView!
override func viewDidLoad()
{
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let TVC = storyboard.instantiateViewController(withIdentifier: "TVC") as! TVC
addChildViewController(TVC)
SV.addSubview(TVC.view)
TVC.didMove(toParentViewController: self)
TVC.view.translatesAutoresizingMaskIntoConstraints = false
TVC.view.topAnchor.constraint(equalTo: SV.topAnchor).isActive = true
TVC.view.leadingAnchor.constraint(equalTo: SV.leadingAnchor, constant: 100).isActive = true
TVC.view.trailingAnchor.constraint(equalTo: SV.trailingAnchor, constant: -100).isActive = true
TVC.view.bottomAnchor.constraint(equalTo: SV.bottomAnchor).isActive = true
}
If I open debugger, I see that the ScrollView layout is ambiguous and that override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell never gets called for the table view controller (why ?).
Instead, if I simply add a label within the storyboard or any other view, inside the scrollview, and add 4 constraints to it, then the TableView will be shown and override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell is called as expected.
What's up with this ? ScrollView shouldn't be ambiguous, because the TableView should calculate it's own intrinsic content size ? What's wrong with the tableview that it never populates itself if it's alone inside the ScrollView ?
I'm adding the whole sampleproject. If you run it, the result is expected, the tableview is shown. If you go to storyboard and remove the label from the ScrollView, then the TableView won't be populated at all.
I don't know how I can directly add the sampleproject here, so I uploaded it. Just run it, then remove the only label from storyboard and re-run it, no more table view.
https://wetransfer.com/downloads/e0a75a995992cdc2be9120224515549920181003074547/1083bc9a3cb34fe9b1ab8e513b1520f020181003074547/e68826
PS: ignore other things, like child view controlers in there. My only question is why the tableview is shown when the label exists inside the same ScrollView and not shown when the label is removed.
First of all, the constraints applied to the views inside a ScrollView are a little bit, let's say, special. They are not linked to the ScrollView's frame, they are sort of linked to the ScrollView's "contentSize". Normally, if you would have a View inside another view, and the inside view is pinned in all directions to the superView, it will either stretch to match the parent size (if the parent constraints are well defined to a different parent), or the parent will "wrap" the child size, if the parent constraints are not defined relative to another parent.
Now the ScrollView behaves a little bit different in this case, the ScrollView size will never "wrap" the child. The child constraints are added just to tell the ScrollView, what's the scroll area (contentSize).
Moreover, in your case the UITableView, you can think of it like a "special" ScrollView, can't provide enough information for the ScrollView in order to compute the scrollableContent. If you pin the tableView in all directions, it will just tell the TableView, "align with the parent in all directions" but the scroll view will not know "how much can I scroll".
So because of the above problem, you usually add a UIView inside the ScrollView to act as a "container" and depending on the scrollDirection, you set the view width/height to be equal to the ScrollView or to the parent of the scrollView (again depending on the use case). Then all the content that you want to be inside the scrollView it's added inside the UIView. Also the content is pinned in such way, that it won't allow "compression" to force the parent view (aka container) to stretch to "wrap" the whole content, in this way, the scroll will know how much is the scroll area.
ScrollView shouldn't be ambiguous, because the TableView should calculate it's own intrinsic content size ?
This one is wrong, the intrinsic content size, defines the minimum size that the view needs in order to be drawn. Now think about this, how much space does a tableView needs in order to be drawn, if no other special implementation is added, the answer is 0. The tableView can have a width/height of 0, there's no constraints to prevent this.
Now to wrap it up, your implementation is a little bit wrong, to fix it, I suggest to have a close look over one of the many tutorials that shows how to implement a UIScrollView with auto-layout.
You'll see that most of the implementations will:
add a UIView inside the UIScrollView (aka "containerView)
then the view will be pinned in all directions to the UIScrollView
then either the width or the height of the container will be set to be equal to the scrollView or the parent of scroll view
then content will be added inside the "containerView"
then constraints will be applied to the content in such way that the container will have to wrap around the content.
Related
I have a main vertical stack view (Frame 1), that has a horizontal paging collection view with flow layout (CollectionView), which has a couple of cells (Cell). Each cell comes with a vertical stack view of a couple of labels (Title, Description).
Is there a way to let the tallest cell determine the collection view's height, while the main stack view width determines the width of the cells?
By default, the collection view is not visible when I use layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize (seemingly it does not "inherit" the cells' height).
For now, I worked around the issue by setting a fixed height for the collection view, and defined the item size manually in the collectionView(_:layout:sizeForItemAt:) (and the cells are flexible).
Since CollectionViews (and TableViews) don't have an intrinsicContentSize, there is no way for the layout engine to know the correct size of your collection without the appropriate constraints. A subclass is needed to provide this property.
In your subclass:
override var intrinsicContentSize: CGSize {
return CGSize(self.contentSize.height, self.superview!.bounds.width)
}
Also, don't forget to invalidate your content size when reloading data:
override func reloadData() {
super.reloadData()
self.invalidateIntrinsicContentSize()
}
For a more detailed explanation on intrinsicContentSize check Apple's Documentation
As the content is static, I think simply using UIScrollView is a pretty viable alternative here (as the dynamic nature of the collection view is not a requirement). The collection view could be a stack embedded into a scroll view, then the contains between the dimensions can be simply set upon initialization.
I'm having a problem with my table view cells as they do not adjust automatically with its content.
I have a label for a title and another label for a name. There is a text view below the two labels which is never displayed when the simulator runs.
This is
what the Table View Cell is supposed to look like, however, this is what the Table View Cell displays.
I have pinned all elements inside the table view cell to the content view using constraints. I read up that adjusting the table view cell height itself will not work, so, I have to adjust the height from the table view itself.
It is set to automatic but it is not adjusting as seen here. I have also tried to set the estimated height to automatic but to no avail. The only solution was to set a custom height but it would look extremely weird if the text view contains only a few text as there would be a large white space. I did not add any code at all to adjust the size.
These are the following constraints:
Table View
Name Label
Title Label
Text View
First You need to add height constraint for textview and add its IBOUTlet then you need to override the updateconstraint of cell and do following in update constraints method.
override func updateConstraints() {
super.updateConstraints()
self.textViewHeightConstraint.constant = self.textView.contentSize.height
}
and also for name label add bottom constraint.
By default the UITextView will not resize itself to fit its content. While you could use #Waqas Sultan approach, I would recommend to let the textView size itself. To achieve that, just use this:
textView.isScrollEnabled = false
Or, in storyboards, find the Scroll Enabled attributed and uncheck it.
This would make textView to size itself.
However, from the constraints you show it is hard to tell if there are really enough constraints to determine the proper frames for all the content - there are constraints related to Review label, but who knows how you constrained that label.
Not seeing all the relevant constraints in the tableView cell, I cannot guarantee that this will be enough to make it work as you expect (you might have forgotten about just a single one constraint, and it might be messing up your whole layout).
Hey buddy i would like you to try this way.
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if indexPath.row == desiredIndexPath { // the index where you want automatic dimension
return UITableViewAutomaticDimension
} else {
return 100 // the height of every other cell.
}
}
note: Make sure that you do not give height to the label. Otherwise the label wont expand according to content.
How to make a UIStackView re-distribute it's sub-UITableViews while the stackView is inside a scrollview?
My layout hierarchy is based on the official documentation from apple about Dynamic content for StackViews
- UISCrollView
- UIStackView
- UIView A
- UIView B
- UIView C
- UITableView X
- UITableView Y
- UIView D
The constraints are set as documented. The initial layout of the StackView is correct showing all visible subviews. When forcing the regular views to expand beyond the screen's height, scrolling is working as expected. Also when viewing the layout in the storyboard, everything stacks as expected.
At this point the UITableViews are empty. As soon as I add content to the tableView the problem appears.
The problem
When I dynamically update the TableView's by calling .reloadData() on both of them I see their content appearing. (thanks to this answer about non-scrolling tableViews) but the UIStackView is not stacking the UITableViews.
UIView D is stacked below UIView C
UITableView X and UITableView Y also stacked below UIView B
My guess is that I need to invalidate the stackview, or somehow get it to redistribute it's subviews. How can I do this?
First, a warning:
What you're trying to achieve is not really standard iOS behavior. You should first consider a different approach like creating a single grouped table view with multiple sections. You can implement custom views inside your table view as section headers or footers.
Now if you really wanna go with your original approach...
... for some important reason you should be aware that a table view doesn't have an intrinsic content size by default. Thus, you need to tell the table view how tall it should be because otherwise it will only shrink down to a zero height.
You can achieve this by either subclassing UITableView and overriding its intrinsicContentSize() as Rob suggests in this answer to a similar question.
Or you add a height constraint to each of your table views and set their constants dynamically in code. A quick example:
Add both your table views to a vertical stack view in Interface Builder.
Give both table views a leading and a trailing constraint to pin their left and right edges to the stack view.
Create outlets for your table views in the respective view controller:
#IBOutlet weak var tableView1: UITableView!
#IBOutlet weak var tableView2: UITableView!
#IBOutlet weak var tableView1HeightConstraint: NSLayoutConstraint!
#IBOutlet weak var tableView2HeightConstraint: NSLayoutConstraint!
Override the updateViewConstraints() method of that view controller:
override func updateViewConstraints() {
super.updateViewConstraints()
tableView1HeightConstraint.constant = tableView1.contentSize.height
tableView2HeightConstraint.constant = tableView2.contentSize.height
}
Now whenever the content of any of your table views changes (e.g. when you add or remove rows or change the cell contents) you need to tell your view controller that it needs to update its constraints. Let's say you have a button that adds a cell to tableView1 each time you tap it. You might implement its action like this:
#IBAction func buttonTappen(sender: AnyObject) {
// custom method you implement somewhere else in your view controller
addRowToTableView1DataSource()
// reload table view with the updated data source
tableView1.reloadData()
// triggers an updateViewConstraints() call
view.setNeedsUpdateConstraints()
}
tl;dr:
A UITableView isn't intended for use without scrolling enabled and thus you always need to explicitly set its height when its contents change - may it be using constraints or by overriding the table view's intrinsic content size.
I'm having trouble getting this working, best explained what i am trying to do with this image of my storyboard.
The main goal is to make that container scrollable, with its size dependent on the view controller that gets added into it via code.
Here is how i have laid out my views.
Obviously my constraints are what is messing me up, but not exactly sure how to get them to behave as expected, I've tried pinning the container to the scroll view and then the scroll view to the main view. The screen appears how i want but it just doesn't scroll.
Any help is much appreciated!
It's impossible from your question & screenshots to determine what constraints you actually do have set up. And your question doesn't even particularly make it clear whether your constraints are behaving properly (aside from the scroll view not scrolling, which may or may not be related to constraints).
First, we need to make sure our constraints are hooked up correctly. The container view for which you're going to put a scroll view into should have four constraints. One for each side, left & right, pinning it to those edges. One for the bottom to pin it to the toolbar, and one for the top to pin it to your top views. It doesn't need any more constraints. Now, the toolbar at the bottom and your views at the top need to have their constraints set up so that they have a constant height per device/orientation, and does not care about the size of the container view. If your constraints are set up correctly, then resizing this view in your interface builder file should be changing the size of the container view. If this isn't happening, head back to the drawing board because something isn't quite right.
If you are confident your constraints are set up correctly, there are a few other things about the scrollview itself that could prevent scrolling.
First, check the most obvious. Is the scrolling enabled property set to true on your scroll view? If not... of course it's not going to scroll.
The other thing that could be happening is that your content view is not larger than the scroll view. Make sure that you've properly set up autolayout for the contents of the scroll view, otherwise the content view will not be larger than the scroll view, and no scrolling will be happening.
So for anyone wondering how to do this, i finally figured out how to do it after a lot of trial and error.
Basically what I did was remove the container view from the storyboard, set my constraints on the scroll view as i normally would:
and then added a container view as a subview of the scroll view in code:
var containerView : UIView!
var currentViewController: UIViewController
override func viewDidLoad() {
super.viewDidLoad()
self.containerView = UIView()
self.scrollView.addSubview(self.containerView)
}
To swap between different view controllers using my UISegmentControl and have the scroll view scroll the content no matter how large the new view controller is i added the following code in my UISegmentControl functions
#IBAction func segControlValueChanged(sender: AnyObject) {
if let vc = viewControllerForSelectedSegmentIndex(sender.selectedSegmentIndex) {
self.currentViewController!.view.removeFromSuperview()
self.currentViewController!.removeFromParentViewController()
displayCurrentTab(sender.selectedSegmentIndex)
}
}
func displayCurrentTab(tabIndex: Int){
if let vc = viewControllerForSelectedSegmentIndex(tabIndex) {
self.addChildViewController(vc)
self.containerView.addSubview(vc.view)
vc.didMoveToParentViewController(self)
vc.view.frame.size = CGSize(width: self.view.frame.width, height: vc.view.frame.height) //had to add this because otherwise for some reason my new vc width would not increase or decrease to its parent view
self.currentViewController = vc
self.scrollView.contentSize = vc.view.frame.size
}
}
func viewControllerForSelectedSegmentIndex(index: Int) -> UIViewController {
//instantiate and return your view controller here
}
Some other notes:
I disabled "resize view from NIB" in storyboard for all my view controllers i plan to add to the container
I am having abit of trouble here trying to make this post description label to grow and shrink based on content size on IOS9. I have a view (I will refer it topView) that I am using as a header for the tableview (So when I scroll up the header disappears). Inside the topView, there are a bunch of stack views. I wish to grow and shrink the post description label in height based on content size. I do know how to do it in simple case where everything is inside the prototype cell (i.e. set estimated row height and set uitableviewautomaticDimensions, set sizetoFit on label and change number of lines to 0). However, this is a different case because the post description label is not really inside the cell, it is in its view before the table view cells.
Note that all items in the view has static height except the postdescription label. Post description label is inside a stack view that is pinned only left and right (So that top and bottom would grow?). Also, the main stack view that contains all elements is pinned towards the four sides with the topview that contains the main stack view also pinned towards the four sides. With this setup, I would expect the topview to grow and shrink based on the content size. However, I do not see that in the output. I dont know if it is the stackview that is holding the label refusing to grow or the top view refusing to grow to allow more space for the stackview for the label. Thanks
UPDATE
Thanks Riadluke, I tried doing something as suggested which is resizing the headerview after calculating the required height. I have placed the following code in viewDidLayoutSubview and it works with an issue
postDescriptionLbl.sizeToFit()
let headerView = commentTableView.tableHeaderView!
headerView.setNeedsLayout()
headerView.layoutIfNeeded()
let height = TopStackView.frame.size.height + ImageStackView.frame.size.height + postDescriptionLbl.frame.size.height + SpacerStackView.frame.size.height + BottomStackView.frame.size.height
headerView.frame.size.height = height
commentTableView.tableHeaderView = headerView
The issue I now have with this method is that when the view controller appears, I can physically see the postDescription label height grow from the default height in storyboard to the required height. For example, when the VC first appears, I see a line of label with some string being cut off, however after 0.5 second, the headerview and the label grow to the size that I wanted. I know this would be expected because I was calling the manipulation after the viewDidLayout Subview. I was wondering if there is a better way such that I dont see that transition and the view appears to be the right height straight away. Ie. let the view know exactly how high the label is to determine how high the headerview needs to be before appearing on screen?
I'm afraid the view set as tableHeaderView of a UITableView does not get resized automatically. Its height will be fixed to the height it had in IB.
What you have to do is set its size manually and then reassign it as the tableHeaderView so it is displayed in the height you want.
It could take only few lines since you're using autolayout.
You can try this code right after you've set the header view's contents:
//for the target size you have set the width as your tableView's width when it is already displayed on screen.
//note that when it is accesed inside viewDidLoad the tableView's bounds
//may be different to the actual bounds it will be displayed with,
//here I am just using the screen bounds
let targetSize = CGSize(width: UIScreen.mainScreen().bounds.width, height: 10000)
//set the tableHeader's size to its size after its layout constraints are resolved
tableHeader.bounds.size = tableHeader.systemLayoutSizeFittingSize(targetSize)
//reassign it as the tableHeaderView to update the height it will be displayed in
tableView.tableHeaderView = tableHeader
After many many attempts, I could not get anything to work with the original setup. The best I achieved was to resize it after view did appear which is not idea as you see the previous layout.
It is now working with a complete different approach. I have created two prototype cell and have one as "HeaderViewCell" and implemented the following functions
commentTableView.estimatedSectionHeaderHeight = 400
commentTableView.sectionHeaderHeight = UITableViewAutomaticDimension
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
Everything works like a charm after that.