uibuttonuistackviewlayoutsubviews

Swift - UIStackView - hide if height of all items is below threshold


I have UIStackView in vertical mode filled with UIButtons. I have dynamic screen resize and if all buttons in stack view have height under some threshold, I want to hide them. How to achive this automatically?

I have tried to extend UIButton and add:

override func layoutSubviews() {
    super.layoutSubviews()
    self.isHidden = (self.frame.height < 20)
}

which works, but once the button is hidden it will never re-appear and layoutSubviews is never called back (even if the height should be again larger).


Solution

  • It's not clear what all you are doing, or why you say it would be problematic to set the buttons' .alpha property, but here are two approaches, both using a UIStackView subclass and handling the show/hide in layoutSubviews().

    1: calculate what the button heights will be and set .isHidden property:

    class MyStackView: UIStackView {
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        func commonInit() -> Void {
            axis = .vertical
            distribution = .fillEqually
            spacing = 8
        }
        override func layoutSubviews() {
            super.layoutSubviews()
    
            // approach 1
            //  setting .isHidden
            let numViews = arrangedSubviews.count
            let numSpaces = numViews - 1
            let h = (bounds.height - (spacing * CGFloat(numSpaces))) / CGFloat(numViews)
            let bHide = h < 20
            arrangedSubviews.forEach { v in
                v.isHidden = bHide
            }
            
        }
        
    }
    
    1. set .isHidden property based on what the button heights are (much simpler):

      class MyStackView: UIStackView {

       override init(frame: CGRect) {
           super.init(frame: frame)
           commonInit()
       }
       required init(coder: NSCoder) {
           super.init(coder: coder)
           commonInit()
       }
       func commonInit() -> Void {
           axis = .vertical
           distribution = .fillEqually
           spacing = 8
       }
       override func layoutSubviews() {
           super.layoutSubviews()
      
           // approach 2
           //  setting .alpha
           arrangedSubviews.forEach { v in
               v.alpha = v.frame.height < 20 ? 0.0 : 1.0
           }
      
       }
      

      }

    And here's a sample controller to see it in use. Tapping anywhere will toggle the height of the stack view between 300 and 100 (buttons will have less-than 20-pts height at 100):

    class ConditionalStackViewController: UIViewController {
        
        let stackView: MyStackView = {
            let v = MyStackView()
            // so we can see the stack view frame
            v.backgroundColor = .systemYellow
            v.translatesAutoresizingMaskIntoConstraints = false
            return v
        }()
        
        var stackHeight: NSLayoutConstraint!
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            for i in 1...6 {
                let b = UIButton()
                b.setTitle("Button \(i)", for: [])
                b.setTitleColor(.white, for: .normal)
                b.setTitleColor(.lightGray, for: .highlighted)
                b.backgroundColor = .red
                stackView.addArrangedSubview(b)
            }
            
            view.addSubview(stackView)
            
            let g = view.safeAreaLayoutGuide
            
            stackHeight = stackView.heightAnchor.constraint(equalToConstant: 300.0)
            
            NSLayoutConstraint.activate([
                stackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
                stackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
                stackView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
                stackHeight,
            ])
            
            let t = UITapGestureRecognizer(target: self, action: #selector(gotTap(_:)))
            view.addGestureRecognizer(t)
        }
        
        @objc func gotTap(_ g: UITapGestureRecognizer) -> Void {
            stackHeight.constant = stackHeight.constant == 300 ? 100 : 300
        }
    
    }