StuckOverFlow
StuckOverFlow

Reputation: 891

UIStackView, arrange subviews only while they fit

I have an array of UIButtons that I am trying to add to an horizontal UIStackView. The number of these buttons is dynamic and they can become that high to not fit on the screen.

For my use case I would like to add the buttons, from the left of my stack view, while they fit on the screen. So if I have 10 buttons, and after adding the 5th one there would not be enough space to add the 6th button, I would like to stop at that moment.

Does anybody knows a good approach to get this behaviour?

My current code looks similar to this:

import UIKit

class ViewController: UIViewController {

    let items = ["One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Eleven"]

    override func viewDidLoad() {
        super.viewDidLoad()

        let stackView = UIStackView()
        stackView.axis = .horizontal
        stackView.spacing = 5
        view.addSubview(stackView)

        for title in items {
            let button = UIButton()
            button.backgroundColor = .red
            button.setTitle(title, for: .normal)

            // if button.doesNotFit() {
            //     break
            // }

            stackView.addArrangedSubview(button)
        }

        let spacerView = UIView()
        spacerView.setContentHuggingPriority(.defaultLow, for: .horizontal)
        stackView.addArrangedSubview(spacerView)

        stackView.translatesAutoresizingMaskIntoConstraints = false
        stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
        stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
        stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
        stackView.heightAnchor.constraint(equalToConstant: 50).isActive = true
    }
}

With that, i get the following result. The problem is in the button "Nine", "Ten", etc... because they do not fit. I would not like them to be added to the UIStackView.

enter image description here

Upvotes: 0

Views: 450

Answers (1)

DonMag
DonMag

Reputation: 77691

What you'll need to do...

  • in viewDidLoad()
    • create the buttons and add them to an array
  • in viewDidLayoutSubviews() (so we have a valid width of the view)
    • get the available width
    • clear any existing buttons from the stack view
    • loop through the buttons array, adding the width of the button + spacing
    • if the width is less than available width, add the button to the stack view

Here is a full example. I included .left, .right and .justified options:

// ButtonsAlignment is
//  left      = buttons will be left-aligned with fixed spacing
//  right     = buttons will be right-aligned with fixed spacing
//  justified = buttons will be justified edge to edge with equal spacing
enum ButtonsAlignment {
    case left
    case right
    case justified
}

class StackSpacingViewController: UIViewController {

    let items = ["One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Eleven"]

    // array to hold your buttons
    var buttonsArray: [UIButton] = []

    // change to .left, .right, .justified to see the difference
    let spacingMode: ButtonsAlignment = .left

    // if mode is .justified, this will be the minimum spacing
    let buttonSpacing: CGFloat = 5.0

    // need to track when the view width changes (when auto-layout has set it)
    //  for use in viewDidLayoutSubviews()
    var viewWidth: CGFloat = 0.0

    // we'll need to refrence the stack view in multiple places,
    //  so don't make it local to viewDidLoad()
    let stackView = UIStackView()

    override func viewDidLoad() {
        super.viewDidLoad()

        stackView.axis = .horizontal

        if spacingMode == .left || spacingMode == .right {
            stackView.distribution = .fill
            stackView.spacing = buttonSpacing
        } else {
            stackView.distribution = .equalSpacing
        }

        view.addSubview(stackView)

        for title in items {
            let button = UIButton()
            button.backgroundColor = .red
            button.setTitle(title, for: .normal)

            // append it to the buttonsArray
            buttonsArray.append(button)
        }

        stackView.translatesAutoresizingMaskIntoConstraints = false

        stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
        stackView.heightAnchor.constraint(equalToConstant: 50).isActive = true

        if spacingMode == .left || spacingMode == .justified {
            stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
        }

        if spacingMode == .right || spacingMode == .justified {
            stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
        }

    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()

        // this can (likely will) be called multiple times
        //  so only update the buttons stack view when the
        //  view's width changes
        if viewWidth != view.frame.width {
            let w = view.frame.width
            viewWidth = w
            // clear any buttons already in the stack
            stackView.subviews.forEach {
                $0.removeFromSuperview()
            }
            var curWidth: CGFloat = 0.0
            for btn in buttonsArray {
                curWidth += btn.intrinsicContentSize.width + buttonSpacing
                if curWidth < w {
                    stackView.addArrangedSubview(btn)
                }
            }
            stackView.setNeedsLayout()
            stackView.layoutIfNeeded()
        }
    }
}

Note: this could (probably would) work better as a self-contained custom view class... would make it easy to add a "background" color, and the calculations could be handled in the subclass's layoutSubviews() function.

Upvotes: 1

Related Questions