0

I have a basic view controller subclass which contains a UIStackView and a UIButton. I want to run some code each time a view is added or removed from the stack view's arrangedSubviews array using Combine. Here's is my failed attempt to do this:

import Combine
import UIKit

class ViewController: UIViewController {
    @IBOutlet var stackView: UIStackView!
    @IBOutlet var button: UIButton!

    var cancelables = Set<AnyCancellable>()

    override func viewDidLoad() {
        super.viewDidLoad()

        stackView.publisher(for: \.arrangedSubviews).sink { views in
            button.isEnabled = views.count < 5
        }
        .store(in: &cancelables)
    }

    @IBAction func addTapped(_ sender: UIButton) {
        let customView = CustomView()
        stackView.addArrangedSubview(customView)
    }
}

Each time I tap the button, a new view is added to the stack view, but the change to arrangedSubviews is not published, which doesn't trigger the code in the sink block. In the case above, the button is still enabled even if arrangedSubviews.count is more than 5.

How can I fix this so that I can correctly publish changes whenever a new view is added or removed from the arrangedSubviews array?

Thank you for any help.

alobaili
  • 761
  • 9
  • 23
  • 1
    Isn't this basically just the standard problem of observing changes to the contents of an array? – matt Jun 13 '22 at 12:14
  • @matt This is what I'm assuming, but I'm not sure. the contents of the array in this case are `UIView`'s which are reference types. I'm not sure if this is what's causing this not to work. Any help or guidance is appreciated. – alobaili Jun 13 '22 at 12:16
  • 2
    AFAIK `publisher(for:` works only on KVO-compliant properties and I don't think that's the case for `UIStackView.arrangedSubviews`. – Fabio Felici Jun 13 '22 at 12:25
  • @FabioFelici Thank you for this info. I didn't know about it before. Is there any other way to react to insertions and removals to `UIStackView.arrangedSubviews`? Even without Combine? – alobaili Jun 13 '22 at 12:28

1 Answers1

0

Thanks to Fabio Felici's comment, I guess UIStackView.arrangedSubviews is not KVO-compliant, which must be what's causing my attempt to not work.

I managed to handle this a different way, by subclassing UIStackView and adding a Published counter which contains the updated value of arrangedSubviews.count each time insertion and removal functions are called.

class CountReactableStackView: UIStackView {
    @Published var numberOfArrangedSubviews = 0
    
    override func addArrangedSubview(_ view: UIView) {
        super.addArrangedSubview(view)
        numberOfArrangedSubviews = arrangedSubviews.count
    }
    
    override func removeArrangedSubview(_ view: UIView) {
        super.removeArrangedSubview(view)
        numberOfArrangedSubviews = arrangedSubviews.count
    }
    
    override func insertArrangedSubview(_ view: UIView, at stackIndex: Int) {
        super.insertArrangedSubview(view, at: stackIndex)
        numberOfArrangedSubviews = arrangedSubviews.count
    }
}

I then subscribe and react to numberOfArrangedSubviews.

Since arrangedSubviews is read-only and can be only changed by the functions above, this simple solution answers my question. I'm still curious if there's a way to observe and react to insertions and deletions to arrangedSubviews directly.

alobaili
  • 761
  • 9
  • 23