skip to Main Content

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

2

Answers


  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.

    Login or Signup to reply.
  2. 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 1nnTap anywhere to rotate center labeln"
            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 2nnTap anywhere to rotate center labeln"
            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 3n"
            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


    Editbased 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

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search