如何用Combine正确监听UIStackView的arrangedSubviews变更
你写的KVO监听无法生效的核心原因是:UIStackView的arrangedSubviews是只读计算属性,并非支持KVO通知的存储属性。
当你调用addArrangedSubview、removeArrangedSubview这类方法时,UIKit是在修改栈视图内部持有的私有视图数组,再通过arrangedSubviews属性把内部数组返回给外部,整个修改过程不会主动给arrangedSubviews属性发送KVO变更通知,自然也就无法触发Combine的sink回调。
另外要注意:哪怕你偶然在某个iOS版本上监听到了变更,也是不稳定的实现,苹果并没有承诺arrangedSubviews支持KVO,直接监听会存在版本兼容风险。
方案1:方法交换拦截所有arrangedSubviews变更(推荐)
这个方案不需要修改你现有调用addArrangedSubview的业务代码,通过runtime方法交换拦截所有会修改arrangedSubviews的系统方法,统一发送变更通知,是覆盖最全面的实现。
- 先给
UIStackView写扩展,实现自定义发布者和方法交换逻辑:
import UIKit import Combine extension Notification.Name { static let stackViewArrangedSubviewsDidChange = Notification.Name("stackViewArrangedSubviewsDidChange") } extension UIStackView { // 对外暴露的arrangedSubviews变更发布者 var arrangedSubviewsChangePublisher: AnyPublisher<[UIView], Never> { NotificationCenter.default.publisher(for: .stackViewArrangedSubviewsDidChange, object: self) .map { _ in self.arrangedSubviews } .prepend(self.arrangedSubviews) // 初始化时先发送一次初始值 .eraseToAnyPublisher() } // 仅执行一次的方法交换逻辑 static let setupArrangedSubviewObserver: Void = { // 拦截addArrangedSubview let originalAdd = class_getInstanceMethod(UIStackView.self, #selector(addArrangedSubview(_:)))! let swizzledAdd = class_getInstanceMethod(UIStackView.self, #selector(swizzled_addArrangedSubview(_:)))! method_exchangeImplementations(originalAdd, swizzledAdd) // 拦截removeArrangedSubview let originalRemove = class_getInstanceMethod(UIStackView.self, #selector(removeArrangedSubview(_:)))! let swizzledRemove = class_getInstanceMethod(UIStackView.self, #selector(swizzled_removeArrangedSubview(_:)))! method_exchangeImplementations(originalRemove, swizzledRemove) // 拦截insertArrangedSubview let originalInsert = class_getInstanceMethod(UIStackView.self, #selector(insertArrangedSubview(_:at:)))! let swizzledInsert = class_getInstanceMethod(UIStackView.self, #selector(swizzled_insertArrangedSubview(_:at:)))! method_exchangeImplementations(originalInsert, swizzledInsert) }() @objc private func swizzled_addArrangedSubview(_ view: UIView) { swizzled_addArrangedSubview(view) NotificationCenter.default.post(name: .stackViewArrangedSubviewsDidChange, object: self) } @objc private func swizzled_removeArrangedSubview(_ view: UIView) { swizzled_removeArrangedSubview(view) NotificationCenter.default.post(name: .stackViewArrangedSubviewsDidChange, object: self) } @objc private func swizzled_insertArrangedSubview(_ view: UIView, at index: Int) { swizzled_insertArrangedSubview(view, at: index) NotificationCenter.default.post(name: .stackViewArrangedSubviewsDidChange, object: self) } }
- 在App启动时执行一次方法交换,比如在
AppDelegate的启动回调里添加:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { UIStackView.setupArrangedSubviewObserver return true }
- 替换你原来ViewController里的监听逻辑即可:
override func viewDidLoad() { super.viewDidLoad() stackView.arrangedSubviewsChangePublisher .sink { [weak self] arrangedViews in self?.button.isEnabled = arrangedViews.count < 5 } .store(in: &cancelables) }
如果需要覆盖「arrangedSubview调用removeFromSuperview()自动从栈视图移除」的场景,只需要额外swizzle UIView的removeFromSuperview方法,判断当前视图的父视图是UIStackView时发送对应通知即可。
方案2:自定义封装增删方法(无swizzle)
如果你的团队禁用runtime方法交换,可以自己封装UIStackView的增删方法,要求所有修改arrangedSubviews的业务逻辑都调用你封装的方法,在方法内部手动发送变更通知:
extension UIStackView { func safeAddArrangedSubview(_ view: UIView) { addArrangedSubview(view) NotificationCenter.default.post(name: .stackViewArrangedSubviewsDidChange, object: self) } func safeRemoveArrangedSubview(_ view: UIView) { removeArrangedSubview(view) NotificationCenter.default.post(name: .stackViewArrangedSubviewsDidChange, object: self) } // insert等其他修改方法按同样逻辑封装 }
这个方案的缺点是需要约束所有开发人员不能直接调用系统原生的增删方法,一旦漏用封装方法就会出现监听不触发的问题。
内容的提问来源于stack exchange,提问作者alobaili

