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.
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 !
Happy to help!
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?
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.
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.
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).
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.
Thanks. It’s very helpful.
Hey John, have you ever ran into this kind of behaviour?
https://stackoverflow.com/questions/46253248/uistackview-proportional-layout-with-only-intrinsiccontentsize
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.
And when you override the intrinsic size, what happens if the stackview’s distribution is `.fill` ?
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.
yeap, absolutely. with using coder init you can do that. that will be a nice way
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)
}
Thank you, Randy! I’m pleased they exposed it more explicitly in Swift 4.
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…. :)
Easy to understand, new discovered the articles. Please keep the sharing these beatiful sources.🎉
Can I please get a working demo for this ?