Search code examples
swiftbuttonautolayoutstackview

Button becomes inactive in programmatic dynamic stackView (Swift)


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
    }
}

enter image description here


Solution

  • 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. enter image description here