Search code examples
iosswiftuikitautolayoutuistackview

How to create horizontal stackview with two child stackviews programmatically?


Some time ago I asked how to draw UI block in this question. Thankfully to @HangarRash I got the answer and understanding how to do it. But right now I would like to created StackView which is based on two other stackviews. I have such code:

class ViewController: UIViewController {
    @IBOutlet weak var mainContainer: UIView!
    
    let colorDictionary = [
        "Red":UIColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0),
        "Green":UIColor(red: 0.0, green: 1.0, blue: 0.0, alpha: 1.0),
        "Blue":UIColor(red: 0.0, green: 0.0, blue: 1.0, alpha: 1.0),
        //        "Green2":UIColor(red: 1.0, green: 0.7, blue: 0.0, alpha: 1.0),
    ]
    
    //MARK: Instance methods
    func colorButton(withColor color:UIColor, title:String) -> UILabel{
        let newButton = UILabel()
        newButton.backgroundColor = .gray
        newButton.text = title
        newButton.textAlignment = .center
        newButton.textColor = UIColor.white
        return newButton
    }
    
    
    
    
    func displayKeyboard(){
        var buttonArray = [UILabel]()
        for (myKey,myValue) in colorDictionary{
            buttonArray += [colorButton(withColor: myValue, title: myKey)]
        }
        
        
        let horizontalStack = UIStackView(arrangedSubviews: buttonArray)
        horizontalStack.axis = .horizontal
        horizontalStack.distribution = .fillEqually
        horizontalStack.alignment = .fill
        horizontalStack.translatesAutoresizingMaskIntoConstraints = false

        let label2 = UILabel()
        label2.text = "Label"
        label2.backgroundColor = .red
        label2.textColor = .white
        label2.textAlignment = .center
        label2.lineBreakMode = .byCharWrapping
        label2.numberOfLines = 0
        label2.translatesAutoresizingMaskIntoConstraints = false

        let leftStack = UIStackView()
        leftStack.backgroundColor = .blue
        leftStack.axis = .vertical
        leftStack.distribution = .equalSpacing
        leftStack.translatesAutoresizingMaskIntoConstraints = false
        leftStack.addArrangedSubview(label2)
        leftStack.addArrangedSubview(horizontalStack)
        leftStack.transform = CGAffineTransform(rotationAngle: -CGFloat.pi / 2)
        
        
        
        
        let bottomStackView = UIStackView(arrangedSubviews: buttonArray)
        bottomStackView.axis = .horizontal
        bottomStackView.distribution = .fillEqually
        bottomStackView.alignment = .fill
        
        
        let stackView = UIStackView()
        stackView.axis = .vertical
        stackView.translatesAutoresizingMaskIntoConstraints = false
        
        
        let url = URL(string: "https://picsum.photos/270")!
        let image = UIImageView()
        
        
        let request = URLRequest(url: url)
        URLSession.shared.dataTask(with: request) { data, _, error in
            if let data = data {
                DispatchQueue.main.async {
                    image.image =  UIImage(data: data)
                    image.contentMode = .scaleToFill
                    image.translatesAutoresizingMaskIntoConstraints = false
                }
            }
        }.resume()
        
        stackView.addArrangedSubview(image)
        stackView.addArrangedSubview(bottomStackView)
        
        
        
        
        let mainStackView = UIStackView()
        mainStackView.axis = .horizontal
        

        mainStackView.addArrangedSubview(leftStack)
        mainStackView.addArrangedSubview(stackView)
        
        mainContainer.addSubview(mainStackView)
        mainStackView.translatesAutoresizingMaskIntoConstraints = false
        
        
        NSLayoutConstraint.activate([
            mainStackView.topAnchor.constraint(equalTo: mainContainer.topAnchor, constant: 5),
            mainStackView.leftAnchor.constraint(equalTo: mainContainer.leftAnchor),
            mainStackView.rightAnchor.constraint(equalTo: mainContainer.rightAnchor),
            mainStackView.heightAnchor.constraint(equalToConstant: 270),
            leftStack.widthAnchor.constraint(equalToConstant: 270),
            
        ])
        
    }
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        displayKeyboard()
    }
}

which draws me such screen:

enter image description here

but I don't understand where is my left ui block with 4 different labels and how to make stackview ui proportionally filled. I mean that I don't need to huge left view, I need it about 10% of the main stackview. I tried to make it in such way:

leftStack.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.1).isActive = true

but it does not help me. I will need like 10% of the left block and 90% of main block with the image. I thought it is possible to set proportions for the views inside the stackview


Solution

  • The problem you are running into is related to how transforms and auto-layout interact - or, perhaps better said, don't interact.

    From Apple's docs:

    In iOS 8.0 and later, the transform property does not affect Auto Layout. Auto layout calculates a view’s alignment rectangle based on its untransformed frame.

    So, when you rotate the stack view, its untransformed frame is used.

    Quick example... Let's put three labels in a horizontal stack view and apply a rotation transform to the center one:

    class Step1VC: UIViewController {
        
        let leftLabel = UILabel()
        let centerLabel = UILabel()
        let rightLabel = UILabel()
    
        override func viewDidLoad() {
            super.viewDidLoad()
            
            view.backgroundColor = .systemYellow
            
            let mainStackView = UIStackView()
            mainStackView.axis = .horizontal
            
            mainStackView.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(mainStackView)
    
            let g = view.safeAreaLayoutGuide
            NSLayoutConstraint.activate([
                mainStackView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
                mainStackView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
            ])
            
            // add three labels to the stack view
            
            leftLabel.textAlignment = .center
            leftLabel.text = "Left"
            leftLabel.backgroundColor = .yellow
            
            centerLabel.textAlignment = .center
            centerLabel.text = "Let's rotate this label"
            centerLabel.backgroundColor = .green
    
            rightLabel.textAlignment = .center
            rightLabel.text = "Right"
            rightLabel.backgroundColor = .cyan
    
            mainStackView.addArrangedSubview(leftLabel)
            mainStackView.addArrangedSubview(centerLabel)
            mainStackView.addArrangedSubview(rightLabel)
            
            // outline the stack view so we can see its frame
            mainStackView.layer.borderColor = UIColor.red.cgColor
            mainStackView.layer.borderWidth = 1
        
            // info label
            let iLabel = UILabel()
            iLabel.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
            iLabel.numberOfLines = 0
            iLabel.textAlignment = .center
            iLabel.text = "\nStep 1\n\nTap anywhere to rotate center label\n"
            iLabel.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(iLabel)
            NSLayoutConstraint.activate([
                iLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -60.0),
                iLabel.centerXAnchor.constraint(equalTo: g.centerXAnchor),
                iLabel.widthAnchor.constraint(equalTo: g.widthAnchor, multiplier: 0.9),
            ])
        }
        
        override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
            if centerLabel.transform == .identity {
                centerLabel.transform = CGAffineTransform(rotationAngle: -CGFloat.pi / 2)
            } else {
                centerLabel.transform = .identity
            }
        }
    }
    

    Here's what we get:

    enter image description here enter image description here

    There are various ways to get around this... thinking about your end-goal, let's create a UIView subclass with a label that will auto-adjust itself when transformed based on the label's frame.

    So, custom class:

    class MyCustomLabelView: UIView {
        
        // public properties to replicate UILabel
        //  add any additional if needed
        
        public var text: String = "" {
            didSet { theLabel.text = text }
        }
        public var textColor: UIColor = .black {
            didSet { theLabel.textColor = textColor }
        }
        public var font: UIFont = .systemFont(ofSize: 17.0) {
            didSet { theLabel.font = font }
        }
        public var textAlignment: NSTextAlignment = .left {
            didSet { theLabel.textAlignment = textAlignment }
        }
        override var backgroundColor: UIColor? {
            didSet {
                theLabel.backgroundColor = backgroundColor
                super.backgroundColor = .clear
            }
        }
        
        private let theLabel = UILabel()
        
        public func rotateTo(_ d: Double) {
            if let v = subviews.first {
                // set the rotation transform
                if d == 0 {
                    self.transform = .identity
                } else {
                    self.transform = CGAffineTransform(rotationAngle: d)
                }
                
                // remove the label
                v.removeFromSuperview()
                
                // tell it to layout itself
                v.setNeedsLayout()
                v.layoutIfNeeded()
                
                // get the frame of the label
                //  apply the same transform
                let r = v.frame.applying(self.transform)
                
                wC.isActive = false
                hC.isActive = false
    
                // add the label back
                addSubview(v)
                
                // set self's width and height anchors
                //  to the width and height of the label
    
                wC = self.widthAnchor.constraint(equalToConstant: r.width)
                hC = self.heightAnchor.constraint(equalToConstant: r.height)
    
                // apply the new constraints
                NSLayoutConstraint.activate([
                    
                    v.centerXAnchor.constraint(equalTo: self.centerXAnchor),
                    v.centerYAnchor.constraint(equalTo: self.centerYAnchor),
    
                    wC, hC
                    
                ])
            }
        }
    
        private var wC: NSLayoutConstraint!
        private var hC: NSLayoutConstraint!
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        private func commonInit() {
            backgroundColor = .clear
            theLabel.translatesAutoresizingMaskIntoConstraints = false
            addSubview(theLabel)
            
            wC = self.widthAnchor.constraint(equalTo: theLabel.widthAnchor)
            hC = self.heightAnchor.constraint(equalTo: theLabel.heightAnchor)
            
            NSLayoutConstraint.activate([
                
                theLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
                theLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
                
                wC, hC,
                
            ])
        }
    }
    

    and the same controller as Step1 but we'll use three MyCustomLabelView instead of three UILabel:

    class Step2VC: UIViewController {
        
        let leftLabel = MyCustomLabelView()
        let centerLabel = MyCustomLabelView()
        let rightLabel = MyCustomLabelView()
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            view.backgroundColor = .systemYellow
            
            let mainStackView = UIStackView()
            mainStackView.axis = .horizontal
            
            mainStackView.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(mainStackView)
            
            let g = view.safeAreaLayoutGuide
            NSLayoutConstraint.activate([
                mainStackView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
                mainStackView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
            ])
            
            // add three labels to the stack view
            
            leftLabel.textAlignment = .center
            leftLabel.text = "Left"
            leftLabel.backgroundColor = .yellow
            
            centerLabel.textAlignment = .center
            centerLabel.text = "Let's rotate this label"
            centerLabel.backgroundColor = .green
            
            rightLabel.textAlignment = .center
            rightLabel.text = "Right"
            rightLabel.backgroundColor = .cyan
            
            mainStackView.addArrangedSubview(leftLabel)
            mainStackView.addArrangedSubview(centerLabel)
            mainStackView.addArrangedSubview(rightLabel)
            
            // outline the stack view so we can see its frame
            mainStackView.layer.borderColor = UIColor.red.cgColor
            mainStackView.layer.borderWidth = 1
            
            // info label
            let iLabel = UILabel()
            iLabel.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
            iLabel.numberOfLines = 0
            iLabel.textAlignment = .center
            iLabel.text = "\nStep 2\n\nTap anywhere to rotate center label\n"
            iLabel.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(iLabel)
            NSLayoutConstraint.activate([
                iLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -60.0),
                iLabel.centerXAnchor.constraint(equalTo: g.centerXAnchor),
                iLabel.widthAnchor.constraint(equalTo: g.widthAnchor, multiplier: 0.9),
            ])
        }
        
        override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
            if centerLabel.transform == .identity {
                centerLabel.rotateTo(-.pi * 0.5)
            } else {
                centerLabel.rotateTo(0)
            }
        }
    }
    

    Now when we rotate the center label (view), we get this:

    enter image description here enter image description here

    So, to get the full layout you're looking for, we'll create a custom view that contains the "left-side" labels (in a couple stack views), and an image view, a stack view for the bottom labels, and an "outer" stack view to hold everything together.

    Custom "left-side" class:

    class MyCustomView: UIView {
        
        public var titleText: String = "" {
            didSet { titleLabel.text = titleText }
        }
        
        public func addLabel(_ v: UIView) {
            labelsStack.addArrangedSubview(v)
        }
        
        public func rotateTo(_ d: Double) {
            
            // get the container view (in this case, it's the outer stack view)
            if let v = subviews.first {
                // set the rotation transform
                if d == 0 {
                    self.transform = .identity
                } else {
                    self.transform = CGAffineTransform(rotationAngle: d)
                }
                
                // remove the container view
                v.removeFromSuperview()
                
                // tell it to layout itself
                v.setNeedsLayout()
                v.layoutIfNeeded()
                
                // get the frame of the container view
                //  apply the same transform as self
                let r = v.frame.applying(self.transform)
                
                wC.isActive = false
                hC.isActive = false
                
                // add it back
                addSubview(v)
                
                // set self's width and height anchors
                //  to the width and height of the container
                wC = self.widthAnchor.constraint(equalToConstant: r.width)
                hC = self.heightAnchor.constraint(equalToConstant: r.height)
                
                // apply the new constraints
                NSLayoutConstraint.activate([
    
                    v.centerXAnchor.constraint(equalTo: self.centerXAnchor),
                    v.centerYAnchor.constraint(equalTo: self.centerYAnchor),
                    wC, hC
    
                ])
            }
        }
        
        // our subviews
        private let outerStack = UIStackView()
        private let titleLabel = UILabel()
        private let labelsStack = UIStackView()
        
        private var wC: NSLayoutConstraint!
        private var hC: NSLayoutConstraint!
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        private func commonInit() {
            
            // stack views and label properties
            
            outerStack.axis = .vertical
            outerStack.distribution = .fillEqually
            
            labelsStack.axis = .horizontal
            labelsStack.distribution = .fillEqually
            
            titleLabel.textAlignment = .center
            titleLabel.backgroundColor = .lightGray
            titleLabel.textColor = .white
            
            // add title label and labels stack to outer stack
            outerStack.addArrangedSubview(titleLabel)
            outerStack.addArrangedSubview(labelsStack)
            
            outerStack.translatesAutoresizingMaskIntoConstraints = false
            addSubview(outerStack)
            
            wC = self.widthAnchor.constraint(equalTo: outerStack.widthAnchor)
            hC = self.heightAnchor.constraint(equalTo: outerStack.heightAnchor)
    
            NSLayoutConstraint.activate([
                
                outerStack.centerXAnchor.constraint(equalTo: self.centerXAnchor),
                outerStack.centerYAnchor.constraint(equalTo: self.centerYAnchor),
                wC, hC,
                
            ])
            
        }
        
    }
    

    and an example controller:

    class Step3VC: UIViewController {
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            view.backgroundColor = .systemYellow
            
            guard let img = UIImage(named: "testPic") else {
                fatalError("Need an image!")
            }
    
            // create the image view
            let imgView = UIImageView()
            imgView.contentMode = .scaleToFill
            imgView.backgroundColor = .systemBlue
            imgView.image = img
            
            // create the "main" stack view
            let mainStackView = UIStackView()
            mainStackView.axis = .horizontal
    
            // create the "right-side" stack view
            let rightSideStack = UIStackView()
            rightSideStack.axis = .vertical
            
            // create the "bottom labels" stack view
            let bottomLabelsStack = UIStackView()
            bottomLabelsStack.distribution = .fillEqually
            
            // add the image view and bottom labels stack view
            //  to the right-side stack view
            rightSideStack.addArrangedSubview(imgView)
            rightSideStack.addArrangedSubview(bottomLabelsStack)
            
            // create the custom "left-side" view
            let myView = MyCustomView()
            
            // add views to the main stack view
            mainStackView.addArrangedSubview(myView)
            mainStackView.addArrangedSubview(rightSideStack)
    
            // add main stack view to view
            mainStackView.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(mainStackView)
    
            let g = view.safeAreaLayoutGuide
            
            NSLayoutConstraint.activate([
                
                // constrain Top/Leading/Trailing
                mainStackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
                mainStackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
                mainStackView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
                
                // main stack view height will be determined by its subviews
                
            ])
    
            // setup the left-side custom view
            myView.titleText = "Gefährdung"
            
            let titles: [String] = [
                "keine / gering", "mittlere", "erhöhte", "hohe",
            ]
            let colors: [UIColor] = [
                UIColor(red: 0.863, green: 0.894, blue: 0.527, alpha: 1.0),
                UIColor(red: 0.942, green: 0.956, blue: 0.767, alpha: 1.0),
                UIColor(red: 0.728, green: 0.828, blue: 0.838, alpha: 1.0),
                UIColor(red: 0.499, green: 0.706, blue: 0.739, alpha: 1.0),
            ]
            
            for (c, t) in zip(colors, titles) {
                myView.addLabel(colorLabel(withColor: c, title: t, titleColor: .black))
            }
            
            // rotate the left-side custom view 90-degrees counter-clockwise
            myView.rotateTo(-.pi * 0.5)
            
            // setup the bottom labels
            let colorDictionary = [
                "Red":UIColor.systemRed,
                "Green":UIColor.systemGreen,
                "Blue":UIColor.systemBlue,
            ]
            
            for (myKey,myValue) in colorDictionary {
                bottomLabelsStack.addArrangedSubview(colorLabel(withColor: myValue, title: myKey, titleColor: .white))
            }
    
            // info label
            let iLabel = UILabel()
            iLabel.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
            iLabel.numberOfLines = 0
            iLabel.textAlignment = .center
            iLabel.text = "\nStep 3\n"
            iLabel.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(iLabel)
            NSLayoutConstraint.activate([
                iLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -60.0),
                iLabel.centerXAnchor.constraint(equalTo: g.centerXAnchor),
                iLabel.widthAnchor.constraint(equalTo: g.widthAnchor, multiplier: 0.9),
            ])
    
        }
        
        func colorLabel(withColor color:UIColor, title:String, titleColor:UIColor) -> UILabel {
            let newLabel = UILabel()
            newLabel.backgroundColor = color
            newLabel.text = title
            newLabel.textAlignment = .center
            newLabel.textColor = titleColor
            return newLabel
        }
    
    }
    

    The result:

    enter image description here

    To improve the visual a bit, I wanted a little "padding" on the labels... so, I used this simple label subclass:

    class PaddedLabel: UILabel {
        var padding: UIEdgeInsets = .zero
        override func drawText(in rect: CGRect) {
            super.drawText(in: rect.inset(by: padding))
        }
        override var intrinsicContentSize : CGSize {
            let sz = super.intrinsicContentSize
            return CGSize(width: sz.width + padding.left + padding.right, height: sz.height + padding.top + padding.bottom)
        }
    }
    

    Replaced the colorLabel(...) func with this:

    func colorLabel(withColor color:UIColor, title:String, titleColor:UIColor) -> UILabel {
        let newLabel = PaddedLabel()
        newLabel.padding = UIEdgeInsets(top: 6, left: 8, bottom: 6, right: 8)
        newLabel.backgroundColor = color
        newLabel.text = title
        newLabel.textAlignment = .center
        newLabel.textColor = titleColor
        return newLabel
    }
    

    and get this final result:

    enter image description here


    Edit - based on comments...

    Slightly modified custom view:

    class MyCustomView: UIView {
        
        public var titleText: String = "" {
            didSet { titleLabel.text = titleText }
        }
        
        public func addLabel(_ v: UIView) {
            labelsStack.addArrangedSubview(v)
        }
        
        public func rotateTo(_ d: Double) {
            
            // get the "container" view (in this case, it's the outer stack view)
            if let v = subviews.first, v == outerStack {
                // set the rotation transform
                if d == 0 {
                    self.transform = .identity
                } else {
                    self.transform = CGAffineTransform(rotationAngle: d)
                }
                
                // remove the container view
                v.removeFromSuperview()
                
                // tell it to layout itself
                v.setNeedsLayout()
                v.layoutIfNeeded()
                
                // get the frame of the container view
                //  apply the same transform as self
                let r = v.frame.applying(self.transform)
                
                wC.isActive = false
                hC.isActive = false
                
                // add it back
                addSubview(v)
                
                // set self's width and height anchors
                //  to the width and height of the container
                wC = self.widthAnchor.constraint(equalToConstant: r.width)
                
                // this will vertically fit to self's superview
                //  if self's superview does not have a constrained height,
                //  self will be sized to the labels combined width
                
                // safely unwrap the superview
                if let sv = self.superview {
                    hC = outerStack.widthAnchor.constraint(equalTo: sv.heightAnchor)
                } else {
                    hC = self.heightAnchor.constraint(equalToConstant: r.height)
                }
                
                // apply the new constraints
                NSLayoutConstraint.activate([
                    
                    v.centerXAnchor.constraint(equalTo: self.centerXAnchor),
                    v.centerYAnchor.constraint(equalTo: self.centerYAnchor),
                    wC, hC,
                    
                ])
                
            }
        }
        
        // our subviews
        private let outerStack = UIStackView()
        private let titleLabel = UILabel()
        private let labelsStack = UIStackView()
        
        private var wC: NSLayoutConstraint!
        private var hC: NSLayoutConstraint!
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        private func commonInit() {
            
            // stack views and label properties
            
            outerStack.axis = .vertical
            outerStack.distribution = .fillEqually
            
            labelsStack.axis = .horizontal
            labelsStack.distribution = .fillEqually
            
            titleLabel.textAlignment = .center
            titleLabel.backgroundColor = .lightGray
            titleLabel.textColor = .white
            
            // add title label and labels stack to outer stack
            outerStack.addArrangedSubview(titleLabel)
            outerStack.addArrangedSubview(labelsStack)
            
            outerStack.translatesAutoresizingMaskIntoConstraints = false
            addSubview(outerStack)
            
            wC = self.widthAnchor.constraint(equalTo: outerStack.widthAnchor)
            hC = self.heightAnchor.constraint(equalTo: outerStack.heightAnchor)
            
            NSLayoutConstraint.activate([
                
                outerStack.centerXAnchor.constraint(equalTo: self.centerXAnchor),
                outerStack.centerYAnchor.constraint(equalTo: self.centerYAnchor),
                wC, hC,
                
            ])
            
        }
        
    }
    

    Modified controller:

    class ViewController: UIViewController {
    
        // make this a class property
        //  so we can reference it outside of viewDidLoad
        let mainStackView = UIStackView()
        
        // for demo purposes
        let minImageViewHeight: CGFloat = 200.0
        
        // we'll be updating the constant on this to show that
        //  the overall height is controlled by the image view height
        var ivHeightConstraint: NSLayoutConstraint!
        
        // we use this to "toggle" between increasing / decreasing the height
        var plusMinus: CGFloat = 1.0
        
        override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    
            // let's increment or decrement the image view Height by 50
            
            // we want 20-points top and bottom within the safe area
            let safeHeight: CGFloat = view.frame.height - (view.safeAreaInsets.top + view.safeAreaInsets.bottom)
            let maxHeight: CGFloat = safeHeight - 40.0
            
            let proposedNewHeight: CGFloat = mainStackView.frame.height + (50.0 * plusMinus)
    
            var currentHeight: CGFloat = ivHeightConstraint.constant
            
            if proposedNewHeight > maxHeight {
                currentHeight += (50.0 - (proposedNewHeight - maxHeight)) + 50.0
                //ivHeight += 50.0
                plusMinus = -1.0
            } else if plusMinus == -1.0, currentHeight - 50.0 < minImageViewHeight {
                currentHeight = minImageViewHeight - 50.0
                plusMinus = 1.0
            }
            
            currentHeight += (50.0 * plusMinus)
    
            ivHeightConstraint.constant = currentHeight
    
        }
    
        override func viewDidLoad() {
            super.viewDidLoad()
            
            view.backgroundColor = .systemYellow
            
            guard let img = UIImage(named: "testPic") else {
                fatalError("Need an image!")
            }
            
            // create the image view
            let imgView = UIImageView()
            imgView.contentMode = .scaleToFill
            imgView.backgroundColor = .systemBlue
            imgView.image = img
            
            // "main" stack view axis
            mainStackView.axis = .horizontal
            
            // create the "right-side" stack view
            let rightSideStack = UIStackView()
            rightSideStack.axis = .vertical
            
            // create the "bottom labels" stack view
            let bottomLabelsStack = UIStackView()
            bottomLabelsStack.distribution = .fillEqually
            
            // add the image view and bottom labels stack view
            //  to the right-side stack view
            rightSideStack.addArrangedSubview(imgView)
            rightSideStack.addArrangedSubview(bottomLabelsStack)
            
            // create the custom "left-side" view
            let myView = MyCustomView()
            
            // add views to the main stack view
            mainStackView.addArrangedSubview(myView)
            mainStackView.addArrangedSubview(rightSideStack)
            
            // add main stack view to view
            mainStackView.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(mainStackView)
            
            let g = view.safeAreaLayoutGuide
            
            ivHeightConstraint = imgView.heightAnchor.constraint(equalToConstant: minImageViewHeight)
            
            NSLayoutConstraint.activate([
                
                // constrain Top/Leading/Trailing
                mainStackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
                mainStackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
                mainStackView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
                
                // we can set the main stack view height here
                //mainStackView.heightAnchor.constraint(equalToConstant: 600.0),
                
                //  or, set the image view height
                // for this example, we'll be setting (and changing) the image view's height
                ivHeightConstraint,
                
            ])
            
            // setup the left-side custom view
            myView.titleText = "Gefährdung"
            
            let titles: [String] = [
                "keine / gering", "mittlere", "erhöhte", "hohe",
            ]
            let colors: [UIColor] = [
                UIColor(red: 0.863, green: 0.894, blue: 0.527, alpha: 1.0),
                UIColor(red: 0.942, green: 0.956, blue: 0.767, alpha: 1.0),
                UIColor(red: 0.728, green: 0.828, blue: 0.838, alpha: 1.0),
                UIColor(red: 0.499, green: 0.706, blue: 0.739, alpha: 1.0),
            ]
            
            for (c, t) in zip(colors, titles) {
                myView.addLabel(colorLabel(withColor: c, title: t, titleColor: .black))
            }
            
            // rotate the left-side custom view 90-degrees counter-clockwise
            myView.rotateTo(-.pi * 0.5)
            
            // setup the bottom labels
            let colorDictionary = [
                "Red":UIColor.systemRed,
                "Green":UIColor.systemGreen,
                "Blue":UIColor.systemBlue,
            ]
            
            for (myKey,myValue) in colorDictionary {
                bottomLabelsStack.addArrangedSubview(colorLabel(withColor: myValue, title: myKey, titleColor: .white))
            }
            
        }
        
        func colorLabel(withColor color:UIColor, title:String, titleColor:UIColor) -> UILabel {
            let newLabel = PaddedLabel()
            newLabel.padding = UIEdgeInsets(top: 6, left: 8, bottom: 6, right: 8)
            newLabel.backgroundColor = color
            newLabel.text = title
            newLabel.textAlignment = .center
            newLabel.textColor = titleColor
            // note: we need to set vertical content hugging priority
            //  so the label don't "stretch"
            newLabel.setContentHuggingPriority(.required, for: .vertical)
            return newLabel
        }
    
    }
    

    Padded label subclass:

    class PaddedLabel: UILabel {
        var padding: UIEdgeInsets = .zero
        override func drawText(in rect: CGRect) {
            super.drawText(in: rect.inset(by: padding))
        }
        override var intrinsicContentSize : CGSize {
            let sz = super.intrinsicContentSize
            return CGSize(width: sz.width + padding.left + padding.right, height: sz.height + padding.top + padding.bottom)
        }
    }
    

    So, with these modifications, we'll use the Height of the image view to determine the overall height, and we'll let the rotated "left-side" custom view fit to the mainStackView.

    We start with an imageView height of 200... each tap will increase that height by 50-points, until we reach a max-height (safe-area minus 20-points top & bottom), at which point each tap will decrease the imageView height.

    Looks about like this:

    enter image description here enter image description here

    enter image description here enter image description here