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 UI开发者_StackOverflow中文版View 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?
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;
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);
});
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);
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];
});
}
精彩评论