The .fillProportionally
property of UIStackView
is (from my experience) one of the most misunderstood elements of auto-layout.
So, I'm not entirely sure this is going to give you what you want, but give it a try.

Tapping the Text
button will change the "description" text and tapping the Height
button will change the height of the "container" view, so you can see how it looks with different amounts of text.
The Report
button will print the resulting heights and proportional ratios of the views.
All via code - no @IBOutlet
or @IBAction
connections - so just start with a new view controller and assign its custom class to ProportionalStackViewController
:
class ProportionalHeightView: UIView {
let myNonScrollTextView: UITextView = {
let v = UITextView()
v.isScrollEnabled = false
v.setContentHuggingPriority(.required, for: .vertical)
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
let padding: CGFloat = 0.0
addSubview(myNonScrollTextView)
myNonScrollTextView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
myNonScrollTextView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: padding),
myNonScrollTextView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -padding),
// if we want the text top-aligned
//myNonScrollTextView.topAnchor.constraint(equalTo: topAnchor),
// if we want the text vertically=sentered
myNonScrollTextView.centerYAnchor.constraint(equalTo: centerYAnchor),
])
}
override func layoutSubviews() {
super.layoutSubviews()
myNonScrollTextView.sizeToFit()
invalidateIntrinsicContentSize()
}
override var intrinsicContentSize: CGSize {
return myNonScrollTextView.bounds.size
}
}
class TitleView: ProportionalHeightView {
override func commonInit() {
super.commonInit()
myNonScrollTextView.font = UIFont.systemFont(ofSize: 22.0, weight: .bold)
myNonScrollTextView.backgroundColor = .cyan
backgroundColor = .blue
}
}
class DescView: ProportionalHeightView {
override func commonInit() {
super.commonInit()
myNonScrollTextView.font = UIFont.systemFont(ofSize: 17.0, weight: .regular)
myNonScrollTextView.backgroundColor = .yellow
backgroundColor = .orange
}
}
class ProportionalStackViewController: UIViewController {
var titleView: ProportionalHeightView = {
let v = ProportionalHeightView()
v.myNonScrollTextView.font = UIFont.systemFont(ofSize: 22.0, weight: .bold)
v.myNonScrollTextView.backgroundColor = .cyan
v.backgroundColor = .blue
return v
}()
var descView: ProportionalHeightView = {
let v = ProportionalHeightView()
v.myNonScrollTextView.font = UIFont.systemFont(ofSize: 16.0, weight: .regular)
v.myNonScrollTextView.backgroundColor = .yellow
v.backgroundColor = .orange
return v
}()
let containerView: UIView = {
let v = UIView()
v.backgroundColor = .white
return v
}()
let proportionalStackView: UIStackView = {
let v = UIStackView()
v.axis = .vertical
v.distribution = .fillProportionally
return v
}()
let changeTextButton: UIButton = {
let b = UIButton()
b.backgroundColor = .gray
b.setTitle("Text", for: .normal)
return b
}()
let changeHeightButton: UIButton = {
let b = UIButton()
b.backgroundColor = .gray
b.setTitle("Height", for: .normal)
return b
}()
let reportButton: UIButton = {
let b = UIButton()
b.backgroundColor = .gray
b.setTitle("Report", for: .normal)
return b
}()
let btnStack: UIStackView = {
let v = UIStackView()
v.distribution = .fillEqually
v.spacing = 20
return v
}()
var nLines = 0
var containerH = NSLayoutConstraint()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemTeal
btnStack.translatesAutoresizingMaskIntoConstraints = false
proportionalStackView.translatesAutoresizingMaskIntoConstraints = false
containerView.translatesAutoresizingMaskIntoConstraints = false
// add a horizontal stack view with buttons at the top
btnStack.addArrangedSubview(changeTextButton)
btnStack.addArrangedSubview(changeHeightButton)
btnStack.addArrangedSubview(reportButton)
view.addSubview(btnStack)
// set text for titleView
titleView.myNonScrollTextView.text = "Pleasanton Panthers"
descView.myNonScrollTextView.text = "A one stop destination for all the Panthers fans! Experience our new futuristic techology-enabled fan experience an much more!" // "Single line"
proportionalStackView.addArrangedSubview(titleView)
proportionalStackView.addArrangedSubview(descView)
containerView.addSubview(proportionalStackView)
view.addSubview(containerView)
// respect safe area
let g = view.safeAreaLayoutGuide
containerH = containerView.heightAnchor.constraint(equalToConstant: 240.0)
NSLayoutConstraint.activate([
// buttons stack 20-pts from top / leading / trailing
btnStack.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
btnStack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
btnStack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
// container view 20-pts from bottom of buttons, 20-pts from leading / trailing
containerView.topAnchor.constraint(equalTo: btnStack.bottomAnchor, constant: 20.0),
containerView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
containerView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
// container view height
containerH,
// constrain stack view 20-pts from top/bottom/leading/trailing to container
proportionalStackView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 20.0),
proportionalStackView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -20.0),
proportionalStackView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 20.0),
proportionalStackView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -20.0),
])
changeTextButton.addTarget(self, action: #selector(changeText), for: .touchUpInside)
changeHeightButton.addTarget(self, action: #selector(changeHeight), for: .touchUpInside)
reportButton.addTarget(self, action: #selector(report), for: .touchUpInside)
[titleView, titleView.myNonScrollTextView, descView, descView.myNonScrollTextView].forEach {
v in
// un-comment next line to clear background colors
//v.backgroundColor = .clear
}
}
@objc func report() -> Void {
let titleTextH = titleView.myNonScrollTextView.frame.size.height
let descTextH = descView.myNonScrollTextView.frame.size.height
let titleViewH = titleView.frame.size.height
let descViewH = descView.frame.size.height
let tRatio = titleTextH / descTextH
let vRatio = titleViewH / descViewH
print("text heights:\t", titleTextH, descTextH)
print("view heights:\t", titleViewH, descViewH)
print("Text view ratio: \(tRatio) view ratio: \(vRatio)")
}
@objc func changeHeight() -> Void {
if containerView.frame.origin.y + containerView.frame.size.height > view.frame.size.height - 20 {
containerH.constant = 220
}
containerH.constant += 20
}
@objc func changeText() -> Void {
nLines += 1
if nLines > 10 {
descView.myNonScrollTextView.text = "A one stop destination for all the Panthers fans! Experience our new futuristic techology-enabled fan experience an much more!" // "Single line"
nLines = 0
return
}
var s = ""
for i in 1..<nLines {
s += "Line \(i)\n"
}
s += "Line \(nLines)"
descView.myNonScrollTextView.text = s
}
}