Observe changes in a UIView's subviews array with KVO
Asked Answered
P

4

16

I have looked around all over for an answer to this and I have tried to implement it, but nothing is working. Basically, I need to be able to observe changes in a VC view's subviews array. If an existing view is removed from that array, I want to be notified about it and run some code.

Is it possible?

EDIT - More information

I am trying to make a fix for a strange edge case bug where rapidly tapping on the UISearchBar of a UISearchDisplayController (very custom) causes the sdController (or rather the managed searchBar in navBar effect) to disappear from the view, but the sdController is STILL ACTIVE. Which means the navBar stays at the -y origin, and the tableView below isn't scrollable.

My original thought was to get to a state where the sdController was active, but the UISearchDisplayControllerContainerView wasn't in the view hierarchy. I tried testing this in the VC's viewDidLayoutSubviews, but alas, when you tap on a search bar and initiate the sdController animation, the sdController is active, and the UISearchDisplayControllerContainerView isn't in the view hierarchy :(.

Peachy answered 6/11, 2014 at 18:35 Comment(4)
Dirty tricks: post notification via NSNotificationCenter where you removing subview and catch it where you need run some code. Or use delegates.Palmary
You can't KVO subviews. However, the system sends layoutSubviews to a view when it has gained or lost subviews (if the superview is in the on-screen view hierarchy). Maybe you can use a custom UIView subclass as the superview, and do what you need in layoutSubviews. If that's not sufficient, edit your question to include more details about why you want to be notified when the subview is removed. We can probably give you a better solution.Meredith
It sounds like you're trying to apply a hack workaround to fix unexpected behaviour that occurs when rapidly tapping an element on your screen. It's confusing when you say that a view controller "disappear from the view" since view controllers aren't visible objects. You should consider investigating what is making your view disappear in the first place and prevent it, rather than trying to retroactively act when it gets removed.Piccalilli
Thanks for the comment. By sdController becoming invisible I meant that the sdController-managed searchBar was no longer visible, leaving the navBar area empty because the real navBar is still stuck at the -y origin.Peachy
M
4

As with most properties in Apple's frameworks subviews is not KVO compliant.

If you control either the subview or the superview you can observe changes to the view hierarchy by subclassing and overriding:

In the superview you have to override...

- (void)willRemoveSubview:(UIView *)subview

... or, if you control the subview, you would override...

- (void)willMoveToSuperview

Both methods are called before the view is removed.

Minnow answered 6/11, 2014 at 20:24 Comment(0)
O
25

You can observe property sublayers of CALayer, which is KVO compliant, instead of UIView subviews.

Ozzie answered 8/4, 2015 at 16:6 Comment(2)
Please provide a link to documentation stating that Core Animation in general or CALayer's subviews in particular are KVO compliant. To my understanding they are not. (Note: KVC compliance does not imply KVO compliance).Minnow
Code: [v.layer addObserver:self forKeyPath:@"sublayers" options:NSKeyValueObservingOptionNew context:nil];Reverend
M
4

As with most properties in Apple's frameworks subviews is not KVO compliant.

If you control either the subview or the superview you can observe changes to the view hierarchy by subclassing and overriding:

In the superview you have to override...

- (void)willRemoveSubview:(UIView *)subview

... or, if you control the subview, you would override...

- (void)willMoveToSuperview

Both methods are called before the view is removed.

Minnow answered 6/11, 2014 at 20:24 Comment(0)
M
2

Swift 3.x

use custom view iike

class ObservableView: UIView {
    weak var delegate:ObservableViewDelegate?
    
    override func didAddSubview(_ subview: UIView) {
        super.didAddSubview(subview)
        delegate?.observableView(self, didAddSubview: subview)
    }
    
    override func willRemoveSubview(_ subview: UIView) {
        super.willRemoveSubview(subview)
        delegate?.observableview(self, willRemoveSubview: subview)
    }
    
}

protocol ObservableViewDelegate: class {
    func observableView(_ view:UIView, didAddSubview:UIView)
    func observableview(_ view:UIView, willRemoveSubview:UIView)
}

//Use

class ProfileViewController1: UIViewController, ObservableViewDelegate {
    @IBOutlet var headerview:ObervableView! //set custom class in storyboard or xib and make outlet connection
    
    override func viewDidLoad() {
        super.viewDidLoad()
        headerview.delegate = self
    }
    
    //Delegate methods
    func observableView(_ view: UIView, didAddSubview: UIView) {
        //do somthing
    }
    
    func observableview(_ view: UIView, willRemoveSubview: UIView) {
        //do somthing
    }
}
Maurreen answered 6/9, 2017 at 18:2 Comment(0)
H
0

Well, I have stumbled upon a similar situation. Basically, if you need a view to observe its parent subviews array, or even its size or any change to it (which is non KVO compliant), so as to keep itself on top for example, you can use a combination of associated values and some swizzling.

First, declare the child, and optionally, since I like to keep this kind of edgy solutions as isolated as possible, embed there a UIView private extension to add the KVO machinery (Swift 5.1 code):

class AlwaysOnTopView: UIView {

    private var observer: NSKeyValueObservation?
    
    override func willMove(toSuperview newSuperview: UIView?) {
        if self.superview != nil {
            self.observer?.invalidate()
        }
        super.willMove(toSuperview: newSuperview)
    }
    
    override func layoutSubviews() {
        self.superview?.bringSubviewToFront(self)
    }
    
    override func didMoveToSuperview() {
        super.didMoveToSuperview()
        
        if self.superview != nil {
            self.layer.zPosition = CGFloat.greatestFiniteMagnitude
            self.superview?.swizzleAddSubview()
            observer = self.superview?.observe(\.subviewsCount, options: .new) { (view, value) in
                guard view == self.superview else {return}
                self.superview?.bringSubviewToFront(self)
            }
        }
    }}

    private extension UIView {
        @objc dynamic var subviewsCount: Int {
            get {
                return getAssociatedValue(key: "subviewsCount", object: self, initialValue: self.subviews.count)
            }
            set {
                self.willChangeValue(for: \.subviewsCount)
                set(associatedValue: newValue, key: "subviewsCount", object: self)
                self.didChangeValue(for: \.subviewsCount)
            }
        }
        
        @objc dynamic func _swizzled_addSubview(_ view: UIView) {
            _swizzled_addSubview(view)
            self.subviewsCount = self.subviews.count
        }
        
        func swizzleAddSubview() {
            let selector1 = #selector(UIView.addSubview(_:))
            let selector2 = #selector(UIView._swizzled_addSubview(_:))
            
            let originalMethod = class_getInstanceMethod(UIView.self, selector1)!
            let swizzleMethod = class_getInstanceMethod(UIView.self, selector2)!
            method_exchangeImplementations(originalMethod, swizzleMethod)
        }
    }
}

This way, you can keep your internal property observable, and aligned with any added view. This is just a quick implementation, there are many corner cases to handle (e.g: views added using insert or any other UIView methods and so on), but it's a starting point. Also, this can be tailored to different needs (observing siblings for example, not only parents, and so on).

Hughhughes answered 22/7, 2021 at 4:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.