16 Comments

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.