1

I have a container stack view and it has 2 other stackviews lets call them stackview 1 and stackview 2. Stackview 2 have a custom UIView with some label.
My issue is with stackview 1 and it has 2 arranged subviews. Axis of this UIStackView is vertical. These subviews are custom UIView. These custom view may contain text or image and height of these custom views is not predefined and they are based on the content (i.e based on the height of the text).

Custom view 1 and custom view 2 are added in stackview 1. Stack view 1 and stack view 2 are added inside another container stack view.

My issue is I'm not able to display both these custom views (which are inside stackview 1) even if I return intrinsicContentSize (based on intrinsicContentSize of textviews) for these custom views in fillProportionally distribution.

If I return a constant width and height in intrinsicContentSize for customview then both views are displayed properly.

Inside my custom view

override var intrinsicContentSize: CGSize {
    let size = CGSize.init(width: 100, height: 100)
    return size
   }  

Screenshot shows this behaviour.

with constant intrinsicContentSize

But if I return size based on the intrinsicContentSize of UITextView (subview of customView, I have disabled scroll for textview) then only customview 2 is displayed and another view (customview 1) is not displayed.

Inside my Custom view

  override var intrinsicContentSize: CGSize {
       let size = textView.intrinsicContentSize
       return size
       }

Screenshot shows this behaviour.

with intrinsicContentSize of UItextView

I want fillProportionally behaviour but I cannot make it to work as I want which is to display both views (custom view 1 and custom view 2). According to documentation fillProportionally uses intrinsicContentSize to fill views proportionally. But why isn't that working in my case even after I overrided intrinsicContentSize var?

Why custom view 1 have zero hight even after overriding it's intrinsic content size?

I want both of these custom views to be displayed inside stack view 1.

I have been stuck here so I would greatly appreciate any help.

Jaffer Sheriff
  • 1,444
  • 13
  • 33
  • It often helps to give your views contrasting background colors to make it easier to see what's happening with the frames. Also, can you add an image of what you *want* the result to be? It's a little difficult to envision how you want the views to expand, since the text itself will remain a fixed size. – DonMag Mar 22 '20 at 14:26
  • What if you say `textView.contentSize` instead of `textView.intrinsicContentSize`? Okay, I'm being lazy and not trying it myself, sorry, but could you just check? – matt Mar 22 '20 at 15:37
  • Yes I tried that earlier and it didn't help @matt – Jaffer Sheriff Mar 22 '20 at 15:39
  • Okay, well, I don't understand the example because there seem to be more views than you talk about. Please _thoroughly_ describe the view hierarchy and the goal. Thanks! – matt Mar 22 '20 at 15:40
  • @JafferSheriff - let's start with the basics... Is `custom view 1` a `UIView` with a label? And `custom view 2` is a `UIView` with a (non-scrolling) text view? Based on that, how do you want your proportion calculated? It's still not clear (from your updated post) how the end result *should* look. – DonMag Mar 22 '20 at 15:44
  • Updated the question. I have container stack view(yellow color view) which have 2 other stackviews (sv1 and sv2). Ignore stackview2 , which is bottom view with text.My issue is with stackview1 (violet in color). I want its subviews to be scaled proportionally but first view is getting hidden (in view hierarchy i found its height to be zero), even if i return `intrinsicContentSize` – Jaffer Sheriff Mar 22 '20 at 15:47
  • @DonMag Both `custom view 1` and `custom view 2` are instances of same class. I want them to get scaled based on their content , which is text they contain. – Jaffer Sheriff Mar 22 '20 at 15:49
  • @DonMag Both custom views have nonscrollable `UITextView` added as subviews. – Jaffer Sheriff Mar 22 '20 at 15:49
  • @JafferSheriff - it's still tough to understand exactly what you want... in fact, I'm not so sure you really *do* want `.fillProportionally`. Can you show an image of exactly how you want the end result to look? – DonMag Mar 22 '20 at 19:28
  • @DonMag I want the end result to look like image 1 but with proper height for custom view 1 based on its content height of "Pleasanton Panthers" text view. I also want to know why my configuration doesn't work and why it hides custom view 1 – Jaffer Sheriff Mar 22 '20 at 19:30
  • @JafferSheriff -- your "image 1" is not showing proportional fill based on the heights of the text. The "description" text is *at least* twice as tall as the "title" text, so "customView2" should be *at least* twice as tall as "customView1" – DonMag Mar 22 '20 at 19:42
  • In image 1 i have set intrinsicContentSize as 100, 100. In image 1 as height of both custom view 1 and 2 are same they are not getting proportional height. In my image 2 Instead of hardcoding the size I returned their text views actual intrinsicContentSize and that made custom view 1 to get zero height. I don't get why is this happening – Jaffer Sheriff Mar 22 '20 at 19:47
  • Is this what you are expecting? https://imgur.com/a/sTaTRLL – DonMag Mar 22 '20 at 20:00
  • @DonMag yes exactly. Height of these both views (custom view 1 and 2) should expand proportionally if the parent stack view have more height. That's all I want to achieve. I really really appreciate ur help mate – Jaffer Sheriff Mar 22 '20 at 20:02

1 Answers1

1

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.

enter image description here enter image description here

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
    }

}
DonMag
  • 69,424
  • 5
  • 50
  • 86