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
}
}
CodePudding user response:
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.
CodePudding user response:
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()
}
}
CodePudding user response:
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)
}
}
