I'm trying to implement a programmatic version of the Dynamic Stack View in Apple's Auto Layout Cookbook. An "Add Item" button is supposed to add new views to a vertical stackView, including a delete button to remove each view. My programmatic code works fine for 1 touch of the "Add Item" button, but then that button becomes inactive. As a result, I can only add 1 item to the stackView. If I used the delete button, the "Add Item" becomes active again. I've included an animated gif to illustrate.
I'm posting both my Programmatic code (which has the problem) and below that the original storyboard-based code (which works fine). I've tried putting a debug breakpoint at the addEntry func, but that didn't help. -Thanks
Programmatic Code ("Add Item" button only works once):
import UIKit
class CodeDynamStackVC: UIViewController {
// MARK: Properties
var scrollView = UIScrollView()
var stackView = UIStackView()
var button = UIButton()
// MARK: UIViewController
override func viewDidLoad() {
super.viewDidLoad()
// Set up the scrollview
let insets = UIEdgeInsets(top: 20, left: 0.0, bottom: 0.0, right: 0.0)
scrollView.contentInset = insets
scrollView.scrollIndicatorInsets = insets
setupInitialVertStackView()
}
//setup initial button inside vertical stackView
func setupInitialVertStackView() {
// make inital "Add Item" button
button = UIButton(type: .system)
button.setTitle("Add Item", for: .normal)
button.setTitleColor(UIColor.blue, for: .normal)
button.addTarget(self, action: #selector(addEntry), for: .touchUpInside)
//enclose button in a vertical stackView
stackView.addArrangedSubview(button)
stackView.axis = .vertical
stackView.alignment = .fill
stackView.distribution = .equalSpacing
stackView.spacing = 5
stackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackView)
let viewsDictionary = ["v0":stackView]
let stackView_H = NSLayoutConstraint.constraints(withVisualFormat: "H:|[v0]|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: viewsDictionary)
let stackView_V = NSLayoutConstraint.constraints(withVisualFormat: "V:|-20-[v0(25)]|", options: NSLayoutFormatOptions(rawValue:0), metrics: nil, views: viewsDictionary)
view.addConstraints(stackView_H)
view.addConstraints(stackView_V)
}
// MARK: Interface Builder actions
func addEntry() {
guard let addButtonContainerView = stackView.arrangedSubviews.last else { fatalError("Expected at least one arranged view in the stack view.") }
let nextEntryIndex = stackView.arrangedSubviews.count - 1
let offset = CGPoint(x: scrollView.contentOffset.x, y: scrollView.contentOffset.y + addButtonContainerView.bounds.size.height)
let newEntryView = createEntryView()
newEntryView.isHidden = true
stackView.insertArrangedSubview(newEntryView, at: nextEntryIndex)
UIView.animate(withDuration: 0.25, animations: {
newEntryView.isHidden = false
self.scrollView.contentOffset = offset
})
}
func deleteStackView(_ sender: UIButton) {
guard let entryView = sender.superview else { return }
UIView.animate(withDuration: 0.25, animations: {
entryView.isHidden = true
}, completion: { _ in
entryView.removeFromSuperview()
})
}
// MARK: Convenience
/// Creates a horizontal stackView entry to place within the parent vertical stackView
fileprivate func createEntryView() -> UIView {
let date = DateFormatter.localizedString(from: Date(), dateStyle: .short, timeStyle: .none)
let number = UUID().uuidString
let stack = UIStackView()
stack.axis = .horizontal
stack.alignment = .center
stack.distribution = .fill
stack.spacing = 8
let dateLabel = UILabel()
dateLabel.text = date
dateLabel.font = UIFont.preferredFont(forTextStyle: UIFontTextStyle.body)
let numberLabel = UILabel()
numberLabel.text = number
numberLabel.font = UIFont.preferredFont(forTextStyle: UIFontTextStyle.caption2)
numberLabel.setContentHuggingPriority(UILayoutPriorityDefaultLow - 1.0, for: .horizontal)
numberLabel.setContentCompressionResistancePriority(UILayoutPriorityDefaultHigh - 1.0, for: .horizontal)
let deleteButton = UIButton(type: .roundedRect)
deleteButton.setTitle("Del", for: UIControlState())
deleteButton.addTarget(self, action: #selector(DynamStackVC.deleteStackView(_:)), for: .touchUpInside)
stack.addArrangedSubview(dateLabel)
stack.addArrangedSubview(numberLabel)
stack.addArrangedSubview(deleteButton)
return stack
}
}
Storyboard-based Code ("Add Item" button always works)
import UIKit
class DynamStackVC: UIViewController {
// MARK: Properties
@IBOutlet weak var scrollView: UIScrollView!
@IBOutlet weak var stackView: UIStackView!
// MARK: UIViewController
override func viewDidLoad() {
super.viewDidLoad()
// Set up the scrollview.
let insets = UIEdgeInsets(top: 20, left: 0.0, bottom: 0.0, right: 0.0)
scrollView.contentInset = insets
scrollView.scrollIndicatorInsets = insets
}
// MARK: Interface Builder actions
@IBAction func addEntry(_: AnyObject) {
guard let addButtonContainerView = stackView.arrangedSubviews.last else { fatalError("Expected at least one arranged view in the stack view.") }
let nextEntryIndex = stackView.arrangedSubviews.count - 1
let offset = CGPoint(x: scrollView.contentOffset.x, y: scrollView.contentOffset.y + addButtonContainerView.bounds.size.height)
let newEntryView = createEntryView()
newEntryView.isHidden = true
stackView.insertArrangedSubview(newEntryView, at: nextEntryIndex)
UIView.animate(withDuration: 0.25, animations: {
newEntryView.isHidden = false
self.scrollView.contentOffset = offset
})
}
func deleteStackView(_ sender: UIButton) {
guard let entryView = sender.superview else { return }
UIView.animate(withDuration: 0.25, animations: {
entryView.isHidden = true
}, completion: { _ in
entryView.removeFromSuperview()
})
}
// MARK: Convenience
/// Creates a horizontal stack view entry to place within the parent `stackView`.
fileprivate func createEntryView() -> UIView {
let date = DateFormatter.localizedString(from: Date(), dateStyle: .short, timeStyle: .none)
let number = UUID().uuidString
let stack = UIStackView()
stack.axis = .horizontal
stack.alignment = .center
stack.distribution = .fillProportionally
stack.spacing = 8
let dateLabel = UILabel()
dateLabel.text = date
dateLabel.font = UIFont.preferredFont(forTextStyle: UIFontTextStyle.body)
let numberLabel = UILabel()
numberLabel.text = number
numberLabel.font = UIFont.preferredFont(forTextStyle: UIFontTextStyle.caption2)
numberLabel.setContentHuggingPriority(UILayoutPriorityDefaultLow - 1.0, for: .horizontal)
numberLabel.setContentCompressionResistancePriority(UILayoutPriorityDefaultHigh - 1.0, for: .horizontal)
let deleteButton = UIButton(type: .roundedRect)
deleteButton.setTitle("Del", for: UIControlState())
deleteButton.addTarget(self, action: #selector(DynamStackVC.deleteStackView(_:)), for: .touchUpInside)
stack.addArrangedSubview(dateLabel)
stack.addArrangedSubview(numberLabel)
stack.addArrangedSubview(deleteButton)
return stack
}
}
I figured it out by putting background colors on all buttons and labels within the dynamic stackView. As you can see in the new animated gif, the cyan color of the "Add Item" button disappears after the first button press. To confirm, I doubled the original height of the button (i.e., from (25) to (50) at the left in the gif), which then allowed for two button pressed before it no longer worked and the cyan background disappeared. This taught me a lot about how the dynamic stackView works, and I hope it will help someone else.