skip to Main Content

I have a UIStackView initially set with 4 buttons. If I need to later swap out the last button with a new button or back to the initial button, how can I do that?

lazy var stackView: UIStackView = {
    let sv = UIStackView()
    sv.axis = .horizontal
    sv.distribution = .fillEqually
    sv.alignment = .fill
    return sv
}()

// ...
var bt4: UIButton!
var bt5: UIButton!

// viewDidLoad
func configureStackView() {

    view.addSubview(stackView)

    stackView.addArrangedSubview(bt1)
    stackView.addArrangedSubview(bt2)
    stackView.addArrangedSubview(bt3)
    stackView.addArrangedSubview(bt4)

    // place stackView at bottom of scene
}

func swapLastButtonInStackViewWithNewButton(_ val: Bool) {

    if val {

        // if true replace bt4 in stackView with bt5

    } else {

        // if false replace bt5 in stackView with bt4
    }
}

3

Answers


  1. You can store the arrange of stack subviews like this:

        lazy var stackViewArrangedSubviews = stackView.arrangedSubviews {
            didSet {
                setStackViewSubviews(with: stackViewArrangedSubviews)
            }
        }
    

    and then

        func setStackViewSubviews(with subviews: [UIView]) {
            stackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
            subviews.forEach { stackView.addArrangedSubview($0) }
        }
    

    and finally implement the swap function like this:

        func swapLastButtonInStackViewWithNewButton(_ val: Bool) {
            
            if val {
                stackViewArrangedSubviews[3] = bt5
                // if true replace bt4 in stackView with bt5
                
            } else {
                stackViewArrangedSubviews[3] = bt4
                // if false replace bt5 in stackView with bt4
            }
        }
    

    This is not perfect, You can improve the code by your need.

    Login or Signup to reply.
  2. UIStackView automatically removes a view when this view is hidden. So basically all you have to do is to properly set the isHidden boolean of button 4 and button 5.

    class ViewController: UIViewController {
        @IBOutlet private weak var button4: UIButton!
        @IBOutlet private weak var button5: UIButton!
        private var showButton5 = false {
            didSet {
                button5.isHidden = !showButton5
                button4.isHidden = showButton5
            }
        }
    
        override func viewDidLoad() {
            super.viewDidLoad()
            showButton5 = false
        }
    
        @IBAction private func toggle() {
            showButton5.toggle()
        }
    }
    

    Login or Signup to reply.
  3. You can do this quite easily, without the need to keep a reference to bt4 and bt5:

    func swapLastButtonInStackViewWithNewButton(_ val: Bool) {
        
        // if true replace bt4 in stackView with bt5
        
        // must have 5 buttons in the stack view
        guard stackView.arrangedSubviews.count == 5 else { return }
        
        stackView.arrangedSubviews[3].isHidden = val
        stackView.arrangedSubviews[4].isHidden = !val
    
    }
    

    If you really want to keep a separate reference to the buttons, and add-to/remove-from the stack view, your can do this:

    func swapLastButtonInStackViewWithNewButton(_ val: Bool) {
        
        // if true replace bt4 in stackView with bt5
        
        let btnToShow: UIButton = val ? bt5 : bt4
    
        // we only want to replace the button if it's not already there
        guard let lastButton = stackView.arrangedSubviews.last as? UIButton,
              lastButton != btnToShow
        else { return }
        
        lastButton.removeFromSuperview()
        stackView.addArrangedSubview(btnToShow)
        
    }
    

    Here are complete examples…

    First, using .isHidden approach:

    class StackViewController: UIViewController {
        
        lazy var stackView: UIStackView = {
            let sv = UIStackView()
            sv.axis = .horizontal
            sv.distribution = .fillEqually
            sv.alignment = .fill
            sv.spacing = 12
            return sv
        }()
        
        override func viewDidLoad() {
            super.viewDidLoad()
            configureStackView()
        }
        
        func configureStackView() {
            
            for i in 1...5 {
                let b = UIButton()
                b.setTitle("(i)", for: [])
                b.backgroundColor = .red
                stackView.addArrangedSubview(b)
            }
            
            // place stackView at bottom of scene
            stackView.translatesAutoresizingMaskIntoConstraints = false
    
            view.addSubview(stackView)
            
            let g = view.safeAreaLayoutGuide
            NSLayoutConstraint.activate([
                stackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 16.0),
                stackView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -16.0),
                stackView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -8.0),
            ])
            
            // add a couple "set val" buttons
            let btnSV: UIStackView = {
                let sv = UIStackView()
                sv.axis = .horizontal
                sv.distribution = .fillEqually
                sv.alignment = .fill
                sv.spacing = 12
                return sv
            }()
            ["True", "False"].forEach { t in
                let b = UIButton()
                b.setTitle(t, for: [])
                b.backgroundColor = .blue
                b.addTarget(self, action: #selector(setTrueFalse(_:)), for: .touchUpInside)
                btnSV.addArrangedSubview(b)
            }
            
            btnSV.translatesAutoresizingMaskIntoConstraints = false
            
            view.addSubview(btnSV)
            
            NSLayoutConstraint.activate([
                btnSV.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 60.0),
                btnSV.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -60.0),
                btnSV.centerYAnchor.constraint(equalTo: g.centerYAnchor),
            ])
    
        }
        
        override func viewWillAppear(_ animated: Bool) {
            super.viewWillAppear(animated)
            
            // start with button "5" hidden
            swapLastButtonInStackViewWithNewButton(false)
        }
    
        @objc func setTrueFalse(_ sender: UIButton) {
            guard let t = sender.currentTitle else { return }
            swapLastButtonInStackViewWithNewButton(t == "True")
        }
        
        func swapLastButtonInStackViewWithNewButton(_ val: Bool) {
            
            // if true replace bt4 in stackView with bt5
            
            // must have 5 buttons in the stack view
            guard stackView.arrangedSubviews.count == 5 else { return }
            
            stackView.arrangedSubviews[3].isHidden = val
            stackView.arrangedSubviews[4].isHidden = !val
    
        }
    
    }
    

    or, using a reference to bt4 and bt5 and adding/removing them:

    class StackViewController: UIViewController {
        
        lazy var stackView: UIStackView = {
            let sv = UIStackView()
            sv.axis = .horizontal
            sv.distribution = .fillEqually
            sv.alignment = .fill
            sv.spacing = 12
            return sv
        }()
        
        var bt4: UIButton!
        var bt5: UIButton!
        
        override func viewDidLoad() {
            super.viewDidLoad()
            configureStackView()
        }
        
        func configureStackView() {
            
            for i in 1...5 {
                let b = UIButton()
                b.setTitle("(i)", for: [])
                b.backgroundColor = .red
                stackView.addArrangedSubview(b)
            }
            
            // place stackView at bottom of scene
            stackView.translatesAutoresizingMaskIntoConstraints = false
            
            view.addSubview(stackView)
            
            let g = view.safeAreaLayoutGuide
            NSLayoutConstraint.activate([
                stackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 16.0),
                stackView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -16.0),
                stackView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -8.0),
            ])
            
            // add a couple "set val" buttons
            let btnSV: UIStackView = {
                let sv = UIStackView()
                sv.axis = .horizontal
                sv.distribution = .fillEqually
                sv.alignment = .fill
                sv.spacing = 12
                return sv
            }()
            ["True", "False"].forEach { t in
                let b = UIButton()
                b.setTitle(t, for: [])
                b.backgroundColor = .blue
                b.addTarget(self, action: #selector(setTrueFalse(_:)), for: .touchUpInside)
                btnSV.addArrangedSubview(b)
            }
            
            btnSV.translatesAutoresizingMaskIntoConstraints = false
            
            view.addSubview(btnSV)
            
            NSLayoutConstraint.activate([
                btnSV.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 60.0),
                btnSV.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -60.0),
                btnSV.centerYAnchor.constraint(equalTo: g.centerYAnchor),
            ])
            
        }
        
        override func viewWillAppear(_ animated: Bool) {
            super.viewWillAppear(animated)
            
            // this would go at the end of configureStackView(), but
            //  we'll put it here to keep the changes obvious
            // references to btn4 and btn5
            guard stackView.arrangedSubviews.count == 5,
                  let b4 = stackView.arrangedSubviews[3] as? UIButton,
                  let b5 = stackView.arrangedSubviews[4] as? UIButton
            else {
                fatalError("Bad setup - stackView does not have 5 buttons!")
            }
            bt4 = b4
            bt5 = b5
            
            // start with button "5" hidden
            swapLastButtonInStackViewWithNewButton(false)
        }
        
        @objc func setTrueFalse(_ sender: UIButton) {
            guard let t = sender.currentTitle else { return }
            swapLastButtonInStackViewWithNewButton(t == "True")
        }
        
        func swapLastButtonInStackViewWithNewButton(_ val: Bool) {
            
            // if true replace bt4 in stackView with bt5
            
            let btnToShow: UIButton = val ? bt5 : bt4
    
            // we only want to replace the button if it's not already there
            guard let lastButton = stackView.arrangedSubviews.last as? UIButton,
                  lastButton != btnToShow
            else { return }
            
            lastButton.removeFromSuperview()
            stackView.addArrangedSubview(btnToShow)
            
        }
        
    }
    

    Edit

    The above code might seem a little overly complicated — but I think that’s more related to all of the setup and "extra" checks.

    As a more straight-forward answer…

    As long as you have setup your stack view and have valid references to bt4 and bt5, all you need to do is this:

    func swapLastButtonInStackViewWithNewButton(_ val: Bool) {
        
        // if true replace bt4 in stackView with bt5
        if val {
            bt4.removeFromSuperview()
            stackView.addArrangedSubview(bt5)
        } else {
            bt5.removeFromSuperview()
            stackView.addArrangedSubview(bt4)
        }
    
    }
    

    That will avoid the animation issues.

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