1

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
    }
}
Lance Samaria
  • 17,576
  • 18
  • 108
  • 256

3 Answers3

1

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.

DonMag
  • 69,424
  • 5
  • 50
  • 86
  • Hi Don, good to hear from you! Hope all is well. @Thanh-NhonNguyen added a similar answer below where he swapped the buttons out using the `.isHidden`. His answer actually worked but there was an issue. I'm swapping the buttons out in a function that also calls `UIView.animate(...)` and what I noticed is the buttons literally jump around when being swapped. For eg bt4 will slide to to the right off of the screen and btn5 will slide in to take its place. It's really weird seeing it. The answer that I accepted doesn't cause that issue. Seems to be a Stackview issue with animations. – Lance Samaria Jan 26 '22 at 10:12
  • Your 2nd approach doesn't use the `.isHidden` property. I'll try it tomorrow and let you know the results. Thanks. – Lance Samaria Jan 26 '22 at 10:20
  • FYI, the animation is for something entirely different, it has nothing to do with the button swap. I'm animating a view from 1 height to another but the button swap happens simultaneously, in the same function call. The button swap is first and the animation is second. It's an odd issue. – Lance Samaria Jan 26 '22 at 10:23
  • @LanceSamaria - OK, dealing with animation is a different issue. Do you ***want*** to see the "button swap" animated? If so, how do you want it to look? If you **do not** want it animated, then you need to update the swap-layout either *before* or *after* your other animation is happening. – DonMag Jan 26 '22 at 12:35
  • I have no choice but to see the buttons swap because you have to press that specific button for the view to animate both up and down (think of the youtube toggle button). I guess I can wait for the view to animate and then swap buttons but it seems more natural for the swap to happen as the view animates as opposed to it happening afterwards. Even though MahshidSharif's answer is constantly looping through the array of buttons, the jumping around issue doesn't occur. I'm still trying to figure out your code, it's a tad bit complicated. I see you add all 5 buttons, then remove then swap – Lance Samaria Jan 27 '22 at 02:58
  • I've been working on some other bugs that I had to fix and send the new version to get reviewed. Once I get a little more clarity on your second answer I'll try it out. – Lance Samaria Jan 27 '22 at 02:59
  • 1
    @LanceSamaria - to hopefully clarify things, see the **Edit** at the end of my answer (which probably should have been at the *beginning* of my answer in the first place!). – DonMag Jan 27 '22 at 13:31
  • It works, thanks! Much easier explanation too, lol. I upvoted your answer. Her answer works and her score is low so I let keep the accepted answer help keep her score higher. Thanks again and happy coding – Lance Samaria Jan 27 '22 at 23:45
0

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.

  • You're doing a lot of extra work there... the stack view ***already*** has an array of buttons (`.arrangedSubviews`). No need to keep a separate array, and no need to remove / re-add all of the buttons. – DonMag Jan 25 '22 at 22:17
  • @DonMag (`. arrangedSubviews`) is get-only, so how can I swap buttons? – Mahshid Sharif Jan 26 '22 at 06:35
  • Mahshid - see how I did it in my answer. – DonMag Jan 26 '22 at 14:45
-1

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()
    }
}
Thanh-Nhon Nguyen
  • 3,402
  • 3
  • 28
  • 41
  • Hey thanks for the answer. Your answer worked but there was an issue. I'm swapping the buttons out in a function that also calls `UIView.animate(...)` and what I noticed is the buttons literally jump around when being swapped. For eg bt4 will slide to to the right off of the screen and bt5 will slide in to take its place. It's really weird. That's why I didn't accept it. Thank you though! – Lance Samaria Jan 26 '22 at 10:16
  • @LanceSamaria maybe in this case you may want to change just the title of the buttons (if the appearance of the 2 buttons are not very different of course). Then logically handle the tap action. But it's your app and you know which works best for you :) – Thanh-Nhon Nguyen Jan 26 '22 at 11:32
  • the buttons use images, not titles. Thank you :) – Lance Samaria Jan 27 '22 at 03:01