-1

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

Dávid Pásztor
  • 51,403
  • 9
  • 85
  • 116
Andrew
  • 1,947
  • 2
  • 23
  • 61
  • What is your final goal here? Do you simply want to add an image to the two rows of labels from your previous question? If so, you don't need any more stack views. Use the code from the previous question and simply add the image view to the main stack view. That's it. Nice and simple. – HangarRash Dec 19 '22 at 15:07
  • the final goal is that on the left side we will have the ui from the previous question and in the center we will have image with bottom labels bar – Andrew Dec 19 '22 at 15:27
  • So the final goal is three rows of buttons in total plus the image? The code you posted in this question tries to reuse `buttonArray` twice in two different stack views. You can't do that. A view can only be in once place. – HangarRash Dec 19 '22 at 15:35
  • @Andrew - do you want your image to be `.scaleToFill` or `.scaleAspectFit` or `.scaleAspectFill`? Do you want your "button" labels to actually be buttons? Do you want the end-result Height to be determined by the buttons? – DonMag Dec 19 '22 at 15:45
  • @Andrew - and... do you want your "buttons" to be actual `UIButton`? So they can be tapped? – DonMag Dec 19 '22 at 15:50
  • @Andrew - and... are you trying to rotate just the label? Or the label and "buttons" from your previous question? – DonMag Dec 19 '22 at 16:03
  • @HangarRash, yes maybe it is not very correct, I thought it is possible to use it twice – Andrew Dec 19 '22 at 16:15
  • @DonMag, I would like to see scale_to_fill, no it will be labels, with no interaction,naming is not very good, I'm trying to have rotated 4 different labels in one ui, which are from the previous question – Andrew Dec 19 '22 at 16:17
  • @Andrew - so, is this the idea? https://i.stack.imgur.com/YCpgZ.png – DonMag Dec 19 '22 at 16:30
  • yes, only as I said and you can see on the image labels bar is needed below the image, but in general it looks very nice – Andrew Dec 19 '22 at 16:35
  • @Andrew - ok ... is this closer? https://i.stack.imgur.com/qMog2.png – DonMag Dec 19 '22 at 16:39
  • uh... 4 items on the left side, and 4 items below the image. You can check also the attached to the question image, so you will see very small labels bar below)) thank you for your help) – Andrew Dec 19 '22 at 16:49

2 Answers2

1

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

DonMag
  • 69,424
  • 5
  • 50
  • 86
  • Thank you soooo much for so informative answer, and as I see I can also set own image container height, but for me it is not very obvious where to set the height of left stackview, I found in the code where we are setting constraints, but height as I see is calculated after the final adding of labels, right? I mean that I tried to set own height anchor but it works only for the image container – Andrew Dec 20 '22 at 05:51
  • @Andrew - you need to be a little more specific... What element do you want to use to set the Height? The "overall" UI (in this case, that would be the `mainStackView`)? Do you want to explicitly set the image view height, and have the rest adapt to that? – DonMag Dec 20 '22 at 14:33
  • I want to explicitly set the image view height, and have the rest adapt to that. for example image will have fixed height by code 300 pixels, and the left block should be the same. I found where to change the image height, but could not change the rest one – Andrew Dec 20 '22 at 14:49
  • @Andrew - my answer was really just to explain what happens with transforms and auto-layout... and I provided just **one of many ways** to get your overall layout. I'll add a modified version to my answer, but I ***strongly recommend*** that you go through the code for that custom view until you understand what it is doing. The key point is that, because you are using a rotation transform, the width and height are effectively swapped. – DonMag Dec 20 '22 at 16:57
  • okay, will try to do it as you suggested) in any other case you will see me here with one more question) – Andrew Dec 20 '22 at 17:14
  • @Andrew - if you get stuck, see the **Edit** to my answer. – DonMag Dec 20 '22 at 17:17
  • okay, thank you so much, I think I will try by self, and then will check that I'm close to your solution) – Andrew Dec 20 '22 at 17:27
  • I only stuck with setting proportins for the left block colored view :( strange that bottom view depending on that. here https://stackoverflow.com/questions/74886589/why-does-my-one-view-disappear-after-setting-the-another-view-size-in-uistackvie, maybe you can check when you will have some time? – Andrew Dec 22 '22 at 09:51
-1

First, create the two child stack views and configure them as desired. For example:

let stackView1 = UIStackView()
stackView1.axis = .vertical
stackView1.alignment = .fill
stackView1.distribution = .fillEqually

let stackView2 = UIStackView()
stackView2.axis = .vertical
stackView2.alignment = .fill
stackView2.distribution = .fillEqually

Next, create the horizontal stack view and add the two child stack views as arranged subviews:

let horizontalStackView = UIStackView()
horizontalStackView.axis = .horizontal
horizontalStackView.alignment = .fill
horizontalStackView.distribution = .fillEqually

horizontalStackView.addArrangedSubview(stackView1)
horizontalStackView.addArrangedSubview(stackView2)

Finally, add the horizontal stack view to your view hierarchy and configure its constraints as desired. For example:

view.addSubview(horizontalStackView)
horizontalStackView.translatesAutoresizingMaskIntoConstraints = false
horizontalStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
horizontalStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
horizontalStackView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
horizontalStackView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true

This will create a horizontal stack view with two child stack views that are arranged side-by-side, filling the entire width of the parent view. You can then add views to the child stack views as needed to create your layout.