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 :(.

SPatel
  • 4,768
  • 4
  • 32
  • 51
Lizza
  • 2,769
  • 5
  • 39
  • 72
  • Dirty tricks: post notification via NSNotificationCenter where you removing subview and catch it where you need run some code. Or use delegates. – Oleg Sobolev Nov 06 '14 at 18:44
  • 2
    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. – rob mayoff Nov 06 '14 at 18:47
  • 2
    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. – Ian MacDonald Nov 06 '14 at 19:27
  • 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. – Lizza Nov 06 '14 at 19:29

4 Answers4

23

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

Vladimir Ozerov
  • 239
  • 2
  • 3
  • 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). – Nikolai Ruhe Dec 16 '15 at 08:43
  • 2
    Code: [v.layer addObserver:self forKeyPath:@"sublayers" options:NSKeyValueObservingOptionNew context:nil]; – Nadzeya Feb 01 '21 at 20:54
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.

Community
  • 1
  • 1
Nikolai Ruhe
  • 81,520
  • 17
  • 180
  • 200
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
    }
}
Nick
  • 3,958
  • 4
  • 32
  • 47
SPatel
  • 4,768
  • 4
  • 32
  • 51
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).

Josef
  • 2,869
  • 2
  • 22
  • 23