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:
- We will start with a new Xcode project (Xcode 8.2.1) and use Swift 3.0.2 as language.
- We will step by step create test
UIViews,UIViewControllers and other required items. - At some point we have a "template" that can be used to make the content expand dynamically by someone who are able.
Step 1: Project Creation
Open Xcode (8.2.1), start a new project.
- Select Tabbed Application. We will create the
UIScrollViewin the first tab. - Set product name to DynamicScrollablePage.
- Select location and create the project.
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.
- In
Main.storyboard,First View(i.e. tab 1) delete the two labels. - Click the
UIViewController(named first). Go to its size inspector, change fromFixedtoFreeformand change the height to 1500. This is only a visual change in the storyboard. - Rename the remaining
UIViewasRootView. - Add a
UIScrollViewinsideRootView. Name itScrollView. Constraints:- ScrollView[Top, Bottom, Leading, Trailing, Width] = RootView[Top, Bottom, Leading, Trailing, Width]. In my experience the width constraint must also be set to ensure covering the entire screen later on.
- Add a
UIViewinsideScrollViewand name itContentView. Constraints:- ContentView[Leading, Trailing, Top, Bottom, Width] = ScrollView[Leading, Trailing, Top, Bottom, Width]. The storyboard will now complain about scrolling height. It will not complain after the steps below.
- Add items to
ContentView:- First add a
UIView, name itRedView. SetRedView[Leading, Trailing, Top] =ContentView[Leading, Trailing, Top]. SetRedView[Height, Background Color] = [150, Red]. - Add a
UILabelbelowRedView, set its name/text toFirstLabel. SetFirstLabel[Leading, Trailing] =ContentView[Leading, Trailing]. SetFirstLabel[Top] =RedView[Bottom]. - Add a
UIViewbelowFirstLabel, name itBlueView. SetBlueView[Leading, Trailing] =ContentView[Leading, Trailing]. SetBlueView[Top] =FirstLabel[Bottom]. SetBlueView[Height, Background Color] = [450, Blue]. - Add a
UILabelbelowBlueView, set its name/text toSecondLabel. SetSecondLabel[Leading, Trailing] =ContentView[Leading, Trailing]. SetSecondLabel[Top] =BlueView[Bottom]. - Add a
UIContainerViewbelowSecondLabel, name itContainerView. SetContainerView[Leading, Trailing] =ContentView[Leading, Trailing]. SetContainerView[Top] =SecondLabel[Bottom]. SetContainerView[Intrinsic size] = [Placeholder] (see Size inspector for theContainerView). 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 - Add a
UILabelat the end, name itBottomLabel. SetBottomLabel[Leading, Trailing] =ContentView[Leading, Trailing]. SetBottomView[Top] =ContainerView[Bottom]. - Finally, control + drag from
ScrollViewtoBottomViewand selectBottom Space to ScrollView. This will ensure that theScrollView's height is correct.
- First add a
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.swiftwith the contents:import UIKit class TableViewCell : UITableViewCell { }Create a
xib/Viewfile, namedTableViewCell.xib. Do the following:- Remove the default
UIViewand replace it with aUITableViewCell. - Add a
UILabelto that cell, name itDataLabel(it will also add a content viewUIView). - Set
UITableViewCell's custom class toTableViewCell. - Set the Table View Cell identifier to
TableViewCellId. In dual-view mode, ctrl+drag the label to the
TableViewCellclass. The result should be:import UIKit class TableViewCell : UITableViewCell { @IBOutlet weak var dataLabel: UILabel! }
- Remove the default
Create a file
DynamicEmbeddedViewController.swiftwith 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/Viewfile, namedDynamicEmbeddedView.xib. Rename the mainUIViewtoContentViewand add three items within theContentView:- Add a
UIView, name itGreenView. SetGreenView[Leading, Trailing, Top] =ContentView[Leading, Trailing, Top]. SetGreenView[Height] = [150]. - Add a
UITableView, name itTableView. SetTableView[Leading, Trailing] =ContentView[Leading, Trailing]. SetTableView[Top] =GreenView[Bottom]. Set Intrinsic size = Placeholder. I am not sure if this is the correct approach. ERROR, SEE FINAL STEPS - Add a
UIViewbelowTableView, name itPurpleView. SetPurpleView[Leading, Trailing] =ContentView[Leading, Trailing]. SetPurpleView[Top] =TableView[Bottom]. - Note: At this point we might need some more constraints in the xib, but I am unsure what and how, if any.
- Set the
File's Owner's custom class toDynamicEmbeddedViewController. - Set the
File's Owner's View outlet toContainerView. - Set the
TableView's dataSource and delegate toFile's Owner. - Add the
IBOutletof theTableViewto theDynamicEmbeddedViewControllerclass.
- Add a
Connect the created
xibandUIViewControllerin theMain.storyboard.- Set the Custom Class of the
ContainerView's output View Controller toDynamicEmbeddedViewController. - Delete the existing
Viewin theContainerViewsoutput View Controller. I am not sure if this is really needed.
- Set the Custom Class of the
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:
- The position of the
ContainerViewis correct (i.e. betweenSecondLabelandBottomLabel), but theBottomLabeldoes not adhere its constraint to be below theContainerView. - The
TableView's height is obviously 0. This can also be seen sincefunc tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)is not called. If we set a height constraint on theTableView, items will show up. - The content size of the
ScrollViewdoes not increase if the size of theContainerViewincreases. - It is also desired to display all items in the dynamic
TableViewat all time and just scroll past them as if they were just static data in theScrollView. - This thing is really messy.
Step 5: The questions!
- How can we make the
ScrollView's content properly wrap all the contents, including the dynamic data in theTableViewliving inside theContainerView? - Are the constraints set up properly?
- Where and how should we calculate the proper heights/content sizes?
- Is all of this really necessary; are there easier ways to achieve this?
Step 6: Fixing the solution after @agibson007's answer:
Add
static let CELL_HEIGHT = 44like this:import UIKit class TableViewCell : UITableViewCell { @IBOutlet weak var dataLabel: UILabel! static let CELL_HEIGHT = 44 }Revert
TableView's intrinsic size toDefaultfromPlaceholder.- Set height constraint of for example 150 on the
TableView. This value must be greater than one cell's height. - Add the height constraint to the
DynamicEmbeddedViewControlleras anIBOutlet. Add code to calculate and set
TableViewheight 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 toDefaultfromPlaceholder.- Set height constraint of for example 150 on the
ContainerView. This value will be updated in code. - Add the height constraint to the
ContainerViewas anIBOutletin theFirstViewController. - Add the
ContainerViewas anIBOutletin theFirstViewController. - Create reference to the
DynamicEmbeddedViewControllerinFirstViewControllerso that it may be referenced for height calculation. Add code to calculate and set
ContainerViewheight constraint. FinalFirstViewControllerclass: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.








