25

I'm looking for a way to be notified when a generic UIView is added or removed from the visible view hierarchy. KVO looked like the perfect thing to use in this case, but observing changes to a view's window or superview properties doesn't do anything. Changes to properties like frame, or backgroundColor work as expected but changed to properties relating to the view hierarchy doesn't seem to ever call observeValueForKeyPath.

I checked to see if UIView supports KVO on those properties by calling automaticallyNotifiesObserversForKey, and UIView reported YES for both, leaving me at a loss. So my questions are:

1) Is there a way to use KVO to be notified of events relating to a view being added/removed to the view hierarchy?

2) If not is there another way to be notified of such events that doesn't involve sub-classing UIView?

Mattia
  • 2,251
  • 1
  • 22
  • 27
  • Trying to use KVO results in the following message: `KVO autonotifying only supports -set: methods that return void. Autonotifying will not be done for invocations of -[WebView _setSuperview:].` I would love to find out if there is a better way to do this, but I haven't found any. :-/ – nomothetis Dec 13 '11 at 15:09

4 Answers4

13

Override this method:

- (void)didMoveToSuperview
{
  UIView *superView = [self superview];
}

And you can override these methods in your custom view for other use:

- (void)willMoveToSuperview:(UIView *)newSuperview;
- (void)didMoveToSuperview;
- (void)willMoveToWindow:(UIWindow *)newWindow;
- (void)didMoveToWindow;
Basheer_CAD
  • 4,908
  • 24
  • 36
11

Here is a way. Is it gross? Yes. Do I recommend such behavior? No. But we're all adults here.

The gist is that you use method_setImplementation to change the implementation of -[UIView didAddSubview:] so you get notified whenever it's called (and you'd do the same thing for willRemoveSubview:). Unfortunately, you will get called for all view hierarchy changes. You'll have to add your own filtering to find the specific views you're interested in.

static void InstallAddSubviewListener(void (^listener)(id _self, UIView* subview))
{
    if ( listener == NULL )
    {
        NSLog(@"listener cannot be NULL.");
        return;
    }

    Method addSubviewMethod = class_getInstanceMethod([UIView class], @selector(didAddSubview:));
    IMP originalImp = method_getImplementation(addSubviewMethod);

    void (^block)(id, UIView*) = ^(id _self, UIView* subview) {
        originalImp(_self, @selector(didAddSubview:), subview);
        listener(_self, subview);
    };

    IMP newImp = imp_implementationWithBlock((__bridge void*)block);
    method_setImplementation(addSubviewMethod, newImp);
}

To use, do something like:

InstallAddSubviewListener(^(id _self, UIView *subview) {
    NSLog(@"-[UIView didAddSubview:]   self=%@, view=%@", _self, subview);
});
Doug Richardson
  • 10,483
  • 6
  • 51
  • 77
  • 2
    Well since no sane answer is likely forthcoming, and that suggestion is too butt ugly not to deserve some sort of prize I'm going to accept this one – Mattia Apr 11 '12 at 00:55
  • 1
    it's so ugly, it's beautiful. – danh Mar 11 '13 at 14:35
2

based on the code by @doug-richardson, why not something a little cleaner that will allow KVO for the superview property?

//Make views announce their change of superviews
    Method method = class_getInstanceMethod([UIView class], @selector(willMoveToSuperview:));
    IMP originalImp = method_getImplementation(method);

    void (^block)(id, UIView*) = ^(id _self, UIView* superview) {
        [_self willChangeValueForKey:@"superview"];
        originalImp(_self, @selector(willMoveToSuperview:), superview);
        [_self didChangeValueForKey:@"superview"];
    };

    IMP newImp = imp_implementationWithBlock((__bridge void*)block);
    method_setImplementation(method, newImp);
  • 2
    Have you tested that? It isn't clear to me that superview will be updated to the new value by the time you call didChangeValueForKey:@"superview". – Doug Richardson Mar 11 '13 at 18:30
  • This is incorrect use but close to a good idea. The problem is that "superview" does not change between the willChange and didChange calls (it's not set yet when you call didChangeValueForKey:). You may need to cover both willChange with a block, and a separate block and IMP for didChange. – dbquarrel Feb 11 '19 at 01:07
2

Here's my solution, using ideas from above, fixing some errors, and making it extensible. You can use this flat out to monitor the superview changing with KVO of some class and its subclasses, it should be plug and play.

Written in C to be simple to understand and fast. You could modify these to be some slick categories of NSObject.

Example use:

add_superview_kvo(UILabel.class);

Then you would have to add your own observer to instances as per normal use.

// simple datatype to neatly manage the various runtime elements
typedef struct {
    Class target;
    SEL cmd;
    Method method;
    IMP imp;
} override_t;

// call to initialize an override struct
static override_t
_override_init(Class target, SEL sel) {
    BOOL instance = YES; // should be able to handle class methods by changing this
    override_t o = {
        .target = target,
        .cmd = sel,
        // note this can return a method from the superclass
        .method = instance ? class_getInstanceMethod(target,sel) : class_getClassMethod(target,sel),
        .imp = method_getImplementation(o.method)
    };
    return o;
};

// applies the runtime patch
static void
_override_patch(override_t o, id _Nonnull block) {
    IMP imp = imp_implementationWithBlock(block);
    // first we try to add the method to the class, if we are
    // dealing with an inherited method from a superclass, our
    // new method will drop right in
    if (!class_addMethod(o.target, o.cmd, imp, method_getTypeEncoding(o.method))){
        // this means we got the original method from the
        // class we're manipulating, so we just overwrite
        // its version
        method_setImplementation(o.method,imp);
    }
}

// pass the class in here that you want to monitor for superview changes
// if you pass in UIView.class it will monitor all views... this may
// generate unnecessary overhead, so you can focus it on a class that
// you want (you will get that class and all its subclasses)
void
add_superview_kvo(Class target)
{
    NSString *keyPath = @"superview";

    override_t override = _override_init(target,@selector(willMoveToSuperview:));
    _override_patch(override,^void(id _self, UIView *superview) {
        [_self willChangeValueForKey:keyPath];
        // note that this is the correct way to call an imp, it must be cast
        ((void(*)(id,SEL,id))override.imp)(_self, override.cmd, superview);
    });

    override = _override_init(target,@selector(didMoveToSuperview));
    _override_patch(override,^void(id _self) {
        ((void(*)(id,SEL))override.imp)(_self, override.cmd);
        [_self didChangeValueForKey:keyPath];
    });
}
dbquarrel
  • 1,446
  • 1
  • 10
  • 8