Even though there are multiple questions and answers to this questions here on SO I just cannot create a UIScrollView with both static and dynamic content (by using a ContainerView) and make the sizes work properly. I will therefore provide a step by step guide until the point where I cannot make any progress and someone can provide a solution. This way we will have a workable sample that can be followed step by step to make it work.
Please note: The output from all of the steps is uploaded to https://github.com/oysteinmyrmo/DynamicScrollablePage for convenience. The test Xcode project can be fetched from there and hacked on further.
Update: After @agibson007's answer, there are a few steps at the end to fix the original steps to a working solution. Errors are noted by stating ERROR, SEE FINAL STEPS.
Goal:
Have a long scrollable UIView page with various static UIViews and a ContainerView with dynamic content. For completeness' sake the dynamic content will consist of some static UIViews and a UITableView that will be expanded to its entire contents. The last element seems to be a reoccurring theme in the various questions I have stumbled upon lately.
Method:
UIViews, UIViewControllers and other required items.Step 1: Project Creation
Open Xcode (8.2.1), start a new project.
UIScrollView in the first tab.Step 2: Initial Changes to the Project
The changes to the UI will be done in the first tab. The procedure is heavily influenced by this answer, but we will add a couple of more items and a ContainerView for our dynamic content.
Main.storyboard, First View (i.e. tab 1) delete the two labels.UIViewController (named first). Go to its size inspector, change from Fixed to Freeform and change the height to 1500. This is only a visual change in the storyboard.UIView as RootView.UIScrollView inside RootView. Name it ScrollView. Constraints:
UIView inside ScrollView and name it ContentView. Constraints:
ContentView:
UIView, name it RedView. Set RedView[Leading, Trailing, Top] = ContentView[Leading, Trailing, Top]. Set RedView[Height, Background Color] = [150, Red].UILabel below RedView, set its name/text to FirstLabel. Set FirstLabel[Leading, Trailing] = ContentView[Leading, Trailing]. Set FirstLabel[Top] = RedView[Bottom].UIView below FirstLabel, name it BlueView. Set BlueView[Leading, Trailing] = ContentView[Leading, Trailing]. Set BlueView[Top] = FirstLabel[Bottom]. Set BlueView[Height, Background Color] = [450, Blue].UILabel below BlueView, set its name/text to SecondLabel. Set SecondLabel[Leading, Trailing] = ContentView[Leading, Trailing]. Set SecondLabel[Top] = BlueView[Bottom].UIContainerView below SecondLabel, name it ContainerView. Set ContainerView[Leading, Trailing] = ContentView[Leading, Trailing]. Set ContainerView[Top] = SecondLabel[Bottom]. Set ContainerView[Intrinsic size] = [Placeholder] (see Size inspector for the ContainerView). Setting the intrinsic size to placeholder tells Xcode that the size of it is defined by its child views (as far as I understand). ERROR, SEE FINAL STEPS
UILabel at the end, name it BottomLabel. Set BottomLabel[Leading, Trailing] = ContentView[Leading, Trailing]. Set BottomView[Top] = ContainerView[Bottom].ScrollView to BottomView and select Bottom Space to ScrollView. This will ensure that the ScrollView's height is correct.Step 3: Create a ViewController with Dynamic Content
Now we will create the actual UIViewController and xib file that will be used to display the dynamic contents. We will create a UITableView inside the xib and thus we will also need a UITableViewCell with a simple label for simplicity.
Create a Swift file, TableViewCell.swift with the contents:
import UIKit
class TableViewCell : UITableViewCell {
}
Create a xib/View file, named TableViewCell.xib. Do the following:
UIView and replace it with a UITableViewCell.UILabel to that cell, name it DataLabel (it will also add a content view UIView).UITableViewCell's custom class to TableViewCell.TableViewCellId.In dual-view mode, ctrl+drag the label to the TableViewCell class. The result should be:
import UIKit
class TableViewCell : UITableViewCell {
@IBOutlet weak var dataLabel: UILabel!
}
Create a file DynamicEmbeddedViewController.swift with the contents:
import UIKit
class DynamicEmbeddedViewController : UIViewController, UITableViewDataSource, UITableViewDelegate
{
@IBOutlet weak var tableView: UITableView!
let data = ["First", "Second", "Third", "Fourth", "Fifth", "Sixth", "Last"]
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UINib(nibName: "TableViewCell", bundle: nil), forCellReuseIdentifier: "TableViewCell")
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath) as! TableViewCell
cell.dataLabel.text = data[indexPath.row]
return cell
}
}
Create a xib/View file, named DynamicEmbeddedView.xib. Rename the main UIView to ContentView and add three items within the ContentView:
UIView, name it GreenView. Set GreenView[Leading, Trailing, Top] = ContentView[Leading, Trailing, Top]. Set GreenView[Height] = [150].UITableView, name it TableView. Set TableView[Leading, Trailing] = ContentView[Leading, Trailing]. Set TableView[Top] = GreenView[Bottom]. Set Intrinsic size = Placeholder. I am not sure if this is the correct approach. ERROR, SEE FINAL STEPS
UIView below TableView, name it PurpleView. Set PurpleView[Leading, Trailing] = ContentView[Leading, Trailing]. Set PurpleView[Top] = TableView[Bottom].File's Owner's custom class to DynamicEmbeddedViewController.File's Owner's View outlet to ContainerView.TableView's dataSource and delegate to File's Owner.IBOutlet of the TableView to the DynamicEmbeddedViewController class.Connect the created xib and UIViewController in the Main.storyboard.
ContainerView's output View Controller to DynamicEmbeddedViewController.View in the ContainerViews output View Controller. I am not sure if this is really needed.
Images of Current Situation:


Step 4: Running the app:
Running the app and scrolling all the way to the bottom, including bounce area, this is the result:

From this we can conclude:
ContainerView is correct (i.e. between SecondLabel and BottomLabel), but the BottomLabel does not adhere its constraint to be below the ContainerView.TableView's height is obviously 0. This can also be seen since func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) is not called. If we set a height constraint on the TableView, items will show up.ScrollView does not increase if the size of the ContainerView increases.TableView at all time and just scroll past them as if they were just static data in the ScrollView.Step 5: The questions!
ScrollView's content properly wrap all the contents, including the dynamic data in the TableView living inside the ContainerView?Step 6: Fixing the solution after @agibson007's answer:
Add static let CELL_HEIGHT = 44 like this:
import UIKit
class TableViewCell : UITableViewCell {
@IBOutlet weak var dataLabel: UILabel!
static let CELL_HEIGHT = 44
}
Revert TableView's intrinsic size to Default from Placeholder.
TableView. This value must be greater than one cell's height.DynamicEmbeddedViewController as an IBOutlet.Add code to calculate and set TableView height constraint. Final class:
import UIKit
class DynamicEmbeddedViewController : UIViewController, UITableViewDataSource, UITableViewDelegate
{
@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var tableViewHeight: NSLayoutConstraint!
let data = ["First", "Second", "Third", "Fourth", "Fifth", "Sixth", "Last"]
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UINib(nibName: "TableViewCell", bundle: nil), forCellReuseIdentifier: "TableViewCell")
// Resize our constraint
let totalHeight = data.count * TableViewCell.CELL_HEIGHT
tableViewHeight.constant = CGFloat(totalHeight)
self.updateViewConstraints()
//in a real app a delegate call back would be good to update the constraint on the scrollview
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath) as! TableViewCell
cell.dataLabel.text = data[indexPath.row]
return cell
}
}
Revert ContainerView's intrinsic size to Default from Placeholder.
ContainerView. This value will be updated in code.ContainerView as an IBOutlet in the FirstViewController.ContainerView as an IBOutlet in the FirstViewController.DynamicEmbeddedViewController in FirstViewController so that it may be referenced for height calculation.Add code to calculate and set ContainerView height constraint. Final FirstViewController class:
import UIKit
class FirstViewController: UIViewController {
@IBOutlet weak var containerView: UIView!
@IBOutlet weak var containerViewHeightConstraint: NSLayoutConstraint!
var dynamicView: DynamicEmbeddedViewController?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
if dynamicView != nil{
dynamicView?.tableView.reloadData()
let size = dynamicView?.tableView.contentSize.height
//cheating on the 300 because the other views in that controller at 150 each
containerViewHeightConstraint.constant = size! + 300
self.view.updateConstraintsIfNeeded()
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if (segue.identifier == "ContainerViewSegue") {
dynamicView = segue.destination as? DynamicEmbeddedViewController
}
}
}
And finally everything works as expected!

Please note: The output from all of the steps is uploaded to https://github.com/oysteinmyrmo/DynamicScrollablePage for convenience. The test Xcode project can be fetched from there and hacked on further.
Overview. UIScrollView is the superclass of several UIKit classes, including UITableView and UITextView . A scroll view is a view with an origin that's adjustable over the content view. It clips the content to its frame, which generally (but not necessarily) coincides with that of the app's main window.
I am sure this has been answered before but I cannot remember if it has been answered down to why people have so much trouble with a scrollview. It comes down to 2 things that you have to know about UIScrollView and Autolayout.
1) The scrollview needs to be able to calculate the width and height of the content that is inside. This helps with deciding if it actually needs to scroll.
2) Some views have a size based on the content inside. Example a UILabel has an "intrinsic" content size. That means if you drag it out onto the storyboard you do not need to set a height or width unless you are trying to constraint it someway. Other examples of views with intrinsic sizes would be a UIButton, UIImageView(if it has an image), UITextView(with scrolling disabled) and other controls that may have text or images in them.
So let's start really simple and drag a UIScrollView onto the storyboard and pin it to the superview. All is good no warnings.

Now drag a UILabel onto the scrollview and take a quick peak at the warnings. ScrollView ambiguous scrollable content(width and height).

Now add a top, bottom,leading, and trailing of 20 for all.
No warnings.

Test 2) Delete the UILabel and drag a UIView onto the scrollview and add top,bottom,leading,and trailing of say 20.
Warning! Warning! Warning!

The problem is that a UIView does not have the ability to size itself and the scrollview does not know how big its content will be so it cannot setup.
IF THIS MAKES SENSE == True continue ELSE goBack
Now here is where it will get more complex depending on how far we go but the concepts above govern the entire process.
Investigating your project and setup you did pretty well to be learning UIScrollview.
Now lets go over your summary and I will comment in line as to some things.
From your quote above
"From this we can conclude: The position of the ContainerView is correct (i.e. between SecondLabel and BottomLabel), but the BottomLabel does not adhere its constraint to be below the ContainerView."
***-> You summary here is actually incorrect. Go to the container above the label and checkmark clip to bounds in interface builder and re run the project and the label will be at the bottom but there will be no green view. Why? It does not know how big it is supposed to be. When you ran it before it loaded best it could and uilabels are clear so when it went outside its bounds it looked like it was not correct.
"The TableView's height is obviously 0. This can also be seen since func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) is not called. If we set a height constraint on the TableView, items will show up."
***-> This is the trickiest to deal with. You would have to load the tableview and get the tableview content size and update a height constraint on the tableview in code to get it to update for the scrollview to resize for the container that is holding the controller.
An alternative would be a stackview that can determine it's height and width if the content it holds has intrinsic content size.
But back to the tableview. You would need to set a >= 40 on the tableview//your row height to start on the dynamic view. After you check your datasource if the count is 0 you would update the constraint to 0 and use a delegate to let the scrollview know to update it's constraint on the dynamicviewcontroller to not show the table. I hope this makes sense. Then conversely if the count is say 10 items in the datasource update the constraint on both the dynamicviewcontroller tableview height constraint to 10 * 40 like so
import UIKit
class DynamicEmbeddedViewController : UIViewController, UITableViewDataSource, UITableViewDelegate
{
@IBOutlet weak var tableViewConstraint: NSLayoutConstraint!
@IBOutlet weak var tableView: UITableView!
let data = ["First", "Second", "Third", "Fourth", "Fifth", "Sixth", "Last"]
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
tableView.register(UINib(nibName: "TableViewCell", bundle: nil), forCellReuseIdentifier: "TableViewCell")
//resize our constraint
let count = data.count
let constant = count * 40
tableViewConstraint.constant = CGFloat(constant)
self.updateViewConstraints()
//in a real app a delegate call back would be good to update the constraint on the scrollview
}
And in the first controller it would look like this.
import UIKit
class FirstViewController: UIViewController {
@IBOutlet weak var containerViewHeightConstraint: NSLayoutConstraint!
@IBOutlet weak var containerView: UIView!
@IBOutlet weak var scrollView: UIScrollView!
var dynamicView : DynamicEmbeddedViewController?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
if dynamicView != nil{
dynamicView?.tableView.reloadData()
//get size
let size = dynamicView?.tableView.contentSize.height
//cheating on the 300 because i see you set the other views in that controller at 150 each
containerViewHeightConstraint.constant = size! + 300
self.view.updateConstraintsIfNeeded()
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if (segue.identifier == "ContainerViewSegue") {
dynamicView = segue.destination as? DynamicEmbeddedViewController
}
}
}
You can see we resize everything to fit. Other than that you got it right. Now to production worthy code. Most times you would know what content would be shown so you can control it. Another way is to use UITableView or UICollectionView for all the content and have different cells that would load based on the content. I hope this post clears it up a bit. I am sure we could continue to add to it but hopefully the concepts covered will be enough. If this answers your questions please show some love for the time it takes to do this. I can also upload this to GitHub if you like but it might be best to go in the repo you started.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With