78

I'm trying to create a container view, with a controller that has a dynamic height, inside a UIScrollView and have it sized automatically using auto layout.

Storyboard illustrating the setup

View Controller A is the scrollview, which has the container view included, along with more content below.

View Controller B is the view controller that I want to have a dynamic size and for all the content to be displayed in full height in View Controller A's Scroll View.

I'm having some problems getting the dynamic size of B to automatically set the size of the Container View in A. However if I set a height constraint on the Container View in A Container View in A to for example 250,

It would be the expected output if View Controller B would also have 250 height. It also works fine for height 1000, so as far as I know, all the auto layout constraints are properly setup. Unfortunately, since the height should actually be dynamic, I would like to avoid setting a height constraint at all.

I'm not sure if there are any settings for view controller B I can set for it to automatically update its size depending on its contents, or if there are any other tricks I've missed. Any help would be much appreciated!

Is there any way to size the Container View in A according to how big the size of View Controller B is without setting a height constraint?

byJeevan
  • 3,728
  • 3
  • 37
  • 60
  • You can set your height constraints as IBOutlet and adapt them dynamically in your code – Randy Jan 26 '16 at 13:09
  • This is the closest I've come to a solution, but is this the easiest way to do this? I was hoping there would be something I've missed that would solve the problem more easily than manually setting a height constraint. – TemptingFriendlyGrison Jan 26 '16 at 13:17
  • 1
    i think you can also use **preferredContentSize** here - just keep changing it, you know? – Fattie Mar 05 '17 at 15:33

5 Answers5

120

Yup, there is. I managed to achieve that kind of behavior in one of my own projects.

All you gotta do is to tell the system that it should not add constraints that mimic the fixed frame set for your root view in Interface Builder. The best place to do this is in your container view controller when your embed segue is triggered:

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    // You might want to check if this is your embed segue here
    // in case there are other segues triggered from this view controller. 
    segue.destinationViewController.view.translatesAutoresizingMaskIntoConstraints = NO;
}

Important:

You gotta make sure that the view that you load into the container is constrained from top to bottom and you need to set the priority of one of the vertical constraints to a value lower than 1000. (It's a good practice to always use the bottom constraint for this.) This is necessary because otherwise Interface Builder will complain — with a good reason:

At design time your root view has a fixed size (height). Now if all your subviews have a fixed height and are connected with fixed constraints that all have the same priority it's impossible to fulfil all these requirements unless the height of your fixed root view coincidentally matches exactly the total height of your subviews and the vertical constraints. If you lower the priority of one of the constraints to 999 Interface Builder knows which constraint to break. At runtime however — when the translatesAutoresizingMaskIntoConstraints property is set as stated above — there is no fixed frame for your root view anymore and the system will use your 999 priority constraint instead.

Interface Builder Screenshot

Mischa
  • 15,816
  • 8
  • 59
  • 117
  • 2
    This worked well, thanks! Had some constraint issues in the editor with the scrollview, but those were solved if I set a height constraint on the container that I removed on build time. – TemptingFriendlyGrison Feb 18 '16 at 09:40
  • 14
    You should be able to fix those constraint issues without having to set and remove constraints on your container view if you go to Size Inspector in Interface Builder, scroll down and choose "Placeholder" from the *Intrinsic Content Size* drop down. You can then set a fixed intrinsic size for your container view that is only used by Interface Builder to layout the view but not applied at runtime. – Mischa Feb 18 '16 at 09:46
  • Working on iOS 9, i found that setting the translatesAutoresizingMaskIntoConstraints = NO in the PARENT view controller (rather than the child container VC) did the trick. I kept my bottom constraint priority at 1000 and that allowed the entire container view to grow. Otherwise, the content of the container view went past the bottom of my container view and I was seeing other weirdness in the layout. Thanks for figuring this out!!! – Kento Apr 22 '16 at 19:32
  • This is not working for me, taking iboutlet of container view and adjusing height working – siva krishna Mar 03 '17 at 12:14
  • Great Help ! Very Thanks! – gunjot singh Jun 06 '17 at 09:54
  • Can also set `translatesAutoresizingMaskIntoConstraints` directly in the storyboard. Add it to the User Defined Runtime Attributes table for the root view. – Nate Whittaker Jun 29 '18 at 19:40
  • God! How did I miss that? Thanks, bro! – Arda Oğul Üçpınar Jan 16 '19 at 15:57
  • Can anyone explain one thing to me: So the container view embeds the child view controller's view inside itself (which is the case when using storyboards).. how does the root view pin its top and bottom edges to the container view given translatesAutoresizingMaskIntoConstraints is off? I see us adding constraints inside the root view itself, but nothing tells the root view how to position itself in the container. – Eman Harout Apr 08 '19 at 08:26
  • @EmanH: It seems like the system is doing that automatically and it totally makes sense: When you enable the embedded view's auto-resizing mask it means that you want to dictate its frame from outside. Thus, "no constraints are created" between the container view and the embedded view – only those constraints that enforce the cell's frame as described by its auto-resizing mask. When you disable the embedded view's auto-resizing mask, you want the view's contents to participate in the layout and so the system automatically creates constraints between the two views. – Mischa Apr 09 '19 at 07:23
  • You can see these constraints in _Debug View Hierarchy_ in Interface Builder (this 3D debugger view thingy) when you select the container view. – Mischa Apr 09 '19 at 07:24
  • Note that the bottom constraint can just be set as >= relation, rather than = relationship with < 1000 priority. – Jay Lee Jul 26 '19 at 01:45
  • @Mischa - I'm trying to add a collectionview controller as child inside a tableviewcontroller's row and this solution isn't working. Do you have any other suggestions? For now I'm having height constraint which is working but I really want everything to be dynamic and get rid of the height constraint – LazyLastBencher Nov 06 '20 at 03:58
30

From the answer of @Mischa I was able to make the height of a containerView dynamic depending on its content doing this:

In the viewController of the containerView write:

  override func loadView() {
    super.loadView()
    view.translatesAutoresizingMaskIntoConstraints = false
  }

And taking care that the vertical constraints in IB are all set. Doing this you do not need to set view.translatesAutoresizingMaskIntoConstraints = false from outside the view controller.

In my case I was trying to resize the container to a tableView inside the viewController. Because the tableView has a flexible height depending on its superview (so all OK for IB), I completed the vertical constraints in code by doing this:

  @IBOutlet private var tableView: UITableView! {
    didSet {
      tableView.addConstraint(tableViewHeight)
    }
  }
  private lazy var tableViewHeight: NSLayoutConstraint = NSLayoutConstraint(item: self.tableView, attribute: NSLayoutAttribute.Height, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1, constant: 0)

And then observe the contentSize height of the tableview and adjust the constant of the tableViewHeight constraint programmatically when needed.

acecilia
  • 908
  • 8
  • 12
17

Swift 4, Xcode 9

The accepted answer alone didn't solve the problem for me.

My hierarchy: ScrollView --> Content View (UIView) --> Views | Container View | Other Views.

I had to add the following constraints to make both the ScrollView and Container dynamically adjust:

  1. ScrollView: Top, Bottom, Leading, Trailing to Superview (Safe Areas)
  2. Content View: Top, Bottom, Leading, Trailing, Equal Width to ScrollView, but also with Equal Heights (constraint with a lower priority: 250).
  3. Views: normal auto-layout constraints.
  4. Container View: Top, Bottom to neighbor views, Leading and Trailing to Safe Area.
  5. Container View's embedded VC: Everything constraint connected vertically with the bottom constraint set to a lower priority, but bigger than the Content View's Equal Height one! In this case, a priority of 500 did the trick.
  6. Set view.translatesAutoresizingMaskIntoConstraints = false in either prepareForSegue() or in loadView() as other answers stated.

Now I have a dynamically adjustable Container View inside an auto-resizing Scroll View.

Teodor Ciuraru
  • 3,417
  • 1
  • 32
  • 39
  • Hi Teodor, I read your answer and I think that you can help me with me problem: https://stackoverflow.com/questions/48797508/twitter-profile-effect Thank your for your help. – delarcomarta Feb 18 '18 at 17:09
  • Any GitHub sample? – Muhammad Hassan Nov 15 '18 at 07:43
  • It is not working when there is a scrollable view in the ContainerView – Hamish May 08 '19 at 14:06
  • 1
    I would upvote this 5 times if I could. This step-by-step answer was simple and the solution to 3 hours of messing with IB constraints. – TM Lynch Dec 06 '19 at 20:45
  • Amazing solution !!! worked for me. But i am seeing another issue now. my embedded table view controller scrolls in-place, however the scroll view has stopped scrolling – jerry Feb 07 '20 at 05:17
4

Building on @Mischa's great solution to this problem, if the container embeds a dynamically sized UITableView, then you need to look at overriding it's contentSize and intrinsicContentSize, as well as setting translatesAutoresizingMaskIntoConstraints = false as per the accepted answer.

When the parent view loads and sets up the container view, the intrinsic height of each of the UITableViewCells is inferred to be 0, therefore the UITableViewDelegate method of:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell

will not be called. This makes the UITableView appear as though it has no content.

Overriding the intrinsic content size to return the actual content size means that the tableview is displayed with the size it's contents require.

A great article by Emilio Peláez goes into more depth on this topic.

Stuart Pattison
  • 173
  • 2
  • 12
1

I tried this solution and worked for me.
In destination(child) view controller try to access to parent view controller like this:

if let parentVC = self.parent as? EmbededContinerViewController {               
   if let myParent = parentVC.parent as? ParentViewController {
      myParent.subViewHeight.constant += 2000
      myParent.subView.layoutIfNeeded()
   }

}

Maybe this not normal solution but worked for me and solved my problem.
I tried this code in Swift 5.1 .

reza_khalafi
  • 6,230
  • 7
  • 56
  • 82