UIStackView Tricks: Proportional Custom UIViews with ‘Fill Proportionally’

Stack of Books

In iOS 9, Apple introduced a very handy new UI concept: the UIStackView. Stack views help us quickly compose sequential “stacks” of views without Auto Layout. UIStackView offers a number of distribution and spacing options in Interface Builder. If you’re unfamiliar with UIStackView, I recommend reading “Exploring UIStackView Distribution Types” first.

In this post, I’ll describe how to use the Fill Proportionally option with any custom view while enjoying fine-grained control over the proportions themselves.

The Problem with Proportional Distribution

UIStackView ensures that its arranged subviews maintain the same proportion to each another as your layout grows and shrinks. However, unlike the other distribution options, views that are proportional must have an intrinsic content size. The trouble with that is not all views have an intrinsic size, including UIView itself. Happily, there is a workaround that allows us to adjust proportions of arbitrary UIViews.

Project Setup and Equal Distribution

For this post, I created a new single-view project and added a vertical UIStackView to the view controller. Now let’s add a new view (.xib file) as well as a corresponding .swift implementation that subclasses UIView.

In the code here, we’ll call our new view CustomView. Be sure you set your view’s custom class in the .xib file. Our view will be empty to demonstrate proportional filling with absolutely no intrinsic content size.


Create a new view


Our custom view in IB

In the view controller, let’s create a function called “addSubviews” that loads three of our custom UIViews from the nib file and programmatically adds them to the vertical stack view. The subviews are added when the view controller is loaded. Finally, set the background color of each subview:


import UIKit

class ViewController: UIViewController {
    @IBOutlet var stackView: UIStackView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
    
        addSubviews()
    }
    
    func addSubviews() {
        let subviews = (0..<3).map { (_) -> CustomView in
            return UINib(nibName: "CustomView", bundle: nil).instantiateWithOwner(nil, 
                options: nil)[0] as! CustomView
        }
        
        subviews[0].backgroundColor = UIColor.redColor()
        subviews[1].backgroundColor = UIColor.greenColor()
        subviews[2].backgroundColor = UIColor.blueColor()
        
        subviews.forEach { (view) in
            stackView.addArrangedSubview(view)
        }
    }
}

In Interface Builder (IB), set the stack view’s fill distribution to Fill Equally. This distribution style doesn’t need an intrinsic content size, so we can run the app and see that our views are laid out equally as we expect.


Custom views distributed equally

Fill Proportionally

Fill Equally works as expected. What if we’d like to define a proportional fill for our UIViews? For this app, I want to set the proportions between red:green:blue to 3:2:1, respectively. To do this, go into IB, change the UIStackView’s distribution setting to Fill Proportionally, and run the app. Here is the result:


The problem

Here is where developers run into trouble. UIViews alone do not have intrinsic content sizes. If you have a custom view containing UI elements (say, arranged using Auto Layout) that you want to distribute given an arbitrary proportion, it won’t work without some more code.

The trick to fixing this issue is to override intrinsicContentSize in our custom UIView. In fact, you don’t even need to provide exact numbers for the size because the UIStackView will manage it for us.

Instead, we can set the size to the desired proportions. We’ll add a single var to our class to allow the caller to adjust the proportions:


class CustomView: UIView {
    var height = 1.0
    
    override func intrinsicContentSize() -> CGSize {
        return CGSize(width: 1.0, height: height)
    }
}

Now we have direct control over the intrinsic height of the UIView!

This approach also works for horizontal stack views. In that case, simply alter the width of the CGSize returned by intrinsicContentSize. If we want the height proportions of the views to be 3:2:1, we can set the heights accordingly:


subviews[0].backgroundColor = UIColor.redColor()
subviews[0].height = 3.0
        
subviews[1].backgroundColor = UIColor.greenColor()
subviews[1].height = 2.0
        
subviews[2].backgroundColor = UIColor.blueColor()
subviews[2].height = 1.0

Running the app, we can see the views have laid out exactly as we wanted.


Perfect!

On the whole, I have found UIStackViews to be an efficient and flexible way to compose user interfaces in iOS. This approach is great because it gives the developer more fine-grained control over how the stack view lays out proportionally, without the bother of Auto Layout.

Conversation
  • Thank you so much ! Your custom CustomView is a very good idea ! I was stuck on this problem during 1 day !
    Your article is very clear, thank you again John !

  • Peter Ent says:

    How would you go about changing the height of, say, two of the CustomViews? For example, making the green one height=1 and the blue one height=2?

    • John Fisher John Fisher says:

      If you just want to views (instead of 3), simply change the addSubviews() code to create only two “CustomView”s in the first place, then set their heights to 1 and 2.

      Hope that helps.

      • Peter Ent says:

        I wanted a top permanent view and then display one of the two remaining views depending on what happens in the top view. What I see that I can do is hide the blue view and then animate the showing of the blue while hiding the green. And then reverse it. It works nicely. I thought I had to change their sizes (e.g., from 0 to 1 and back again) but using .isHidden works very well. Thanks for writing this article it was a tremendous help as I was getting all sorts of constraint errors trying to figure it out myself.

        • John Fisher John Fisher says:

          Glad you figured it out! Yes, we’ve had good luck with isHidden as well.

          One issue to be aware of with UIStackViews that we’ve found on projects is– if you use a lot of them, especially in reusable UI elements like table view cells– the adding/removing of views can lead to poor performance (e.g., a less-than-smooth scrolling of your table view).

          • Peter Ent says:

            Thanks. What I’ve done is create 4 UIViewControllers in Interface Builder. In one (MainVC), there is a UIStackView pinned on all 4 sides. The three others (red, green, blue) are added to the stackView in MainVC.viewDidLoad():

            each is loaded via storyboard.instantiateViewController() and then addChildViewController to this main view controller.

            Note: the base UIView of each (red, green, blue) VC has its class set to CustomView so it will deliver an intrinsic size.

            The redVC.view is set with a height and high content hugging priority. Then redVC.view is added to the stackView’s arranged subviews.

            The blue and green VC.view are also set with a height, but their content hugging priority is set lower. The blueVC.isHidden = true while green is visible. blueVC.view and greenVC.view are also added to the stackView’s arranged subviews.

            A button in the redVC causes the blueVC.view and greenVC.view isHidden values to switch within a UIView.annimate block.

            Its very cool, and does exactly what I want: use IB to design my interfaces, keeps the complexity of the content of red, blue, green controllers separate (I use a delegate to communicate the actions in redVC to the mainVC), all the while making things look and behave great.

            I can’t thank you enough for pointing me in the right direction.

  • Jitendra Solanki says:

    Thanks. It’s very helpful.

    • John Fisher John Fisher says:

      Arthur,

      Not specifically. One thing I find helpful is to use view hierarchy debugging to visualize where the views are being placed. Run your project in Xcode and go to:

      Debug -> View Debugging -> Capture View Hierarchy

      The above tool will show you a 3D model of your app, which can help diagnose issues with view placement.

      • Yea, I sure know about this. I used It and found out that width constraint for last view in stackView is disabled, that knowledge, unfortunately, doesn’t help me understand this odd UIStackView behaviour.

  • Mohamed says:

    And when you override the intrinsic size, what happens if the stackview’s distribution is `.fill` ?

  • Danil says:

    I suggest to define height property as @IBInspectable:
    @IBInspectable var height: CGFloat = 1.0
    With this attribute you can change the height parameter from Interface Builder.

    • Elia says:

      yeap, absolutely. with using coder init you can do that. that will be a nice way

  • Randy Weinstein says:

    Great post. In Swift4, intrinsicContentSize is exposed as a computed variable on the UIView API instead of as a method.

    override open var intrinsicContentSize: CGSize {
    return CGSize(width: 1.0, height: height)
    }

    • John Fisher John Fisher says:

      Thank you, Randy! I’m pleased they exposed it more explicitly in Swift 4.

      • Randy Weinstein says:

        Np. I’ve got a UI requirement that involves an expanding UITextField absolutely constrained on its right side that can grow in size leftward. It must maintain absolute spacing with a UILabel on its left side. And the two subviews are constrained by a UIButton to the left of the UILabel. I’ve got a Plan A imp sans an encapsulating StackView that can work, but I think wrapping the two ( or three ) elements in a horizontal StackView (Plan B ) may simplify the implementation. Might be able to eliminate any support code but I’m skeptical. Think I have to programmatically adjust intrinsicContentSize of UITextField no matter what.

        Trying to persuade design team to just make the UITextField a large enough width to accommodate all but the most extreme edge cases and be able to manage display requirements with constraints set in SB and call it a day (Plan C). Fingers crossed for approval of Plan C…. :)

  • Elia says:

    Easy to understand, new discovered the articles. Please keep the sharing these beatiful sources.🎉

  • JB says:

    Can I please get a working demo for this ?

  • Comments are closed.