观察UIView的window和superview属性的变化

25

我正在寻找一种方法,在将一个通用的UIView添加或从可见视图层次结构中删除时得到通知。在这种情况下,KVO看起来是使用的完美方式,但是观察视图的窗口或superview属性的更改并没有任何作用。像frame或backgroundColor这样的属性的更改按预期工作,但是与视图层次结构相关的属性的更改似乎从未调用observeValueForKeyPath。

我通过调用automaticallyNotifiesObserversForKey来检查UIView是否支持对这些属性进行KVO,并且UIView为两者都报告了YES,让我感到困惑。所以我的问题是:

1)有没有办法使用KVO通知与将视图添加/删除到视图层次结构有关的事件?

2)如果没有,有没有其他的方法可以通知此类事件,而无需子类化UIView?


尝试使用KVO会导致以下消息:KVO自动通知仅支持返回void的-set<Key>:方法。对于-[WebView _setSuperview:]的调用将不会进行自动通知。我很想找到更好的方法来解决这个问题,但是我还没有找到。 :-/ - nomothetis
4个回答

13

重写这个方法:

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

你可以在自定义视图中重写这些方法以供其他用途:

- (void)willMoveToSuperview:(UIView *)newSuperview;
- (void)didMoveToSuperview;
- (void)willMoveToWindow:(UIWindow *)newWindow;
- (void)didMoveToWindow;

优秀的回答... 这应该是被采纳的答案。 - eharo2
1
这需要子类化。OP正在寻找一种实现相同结果但不需要子类化的方法。 - pseudosudo

11

这是一种方法。很丑陋吗? 是的。我推荐这样的行为吗? 不。但我们都是成年人了。

要点在于使用 method_setImplementation 来更改 -[UIView didAddSubview:] 的实现,以便每次调用时都会收到通知(并且您将对willRemoveSubview:执行相同的操作)。不幸的是,您将为所有视图层次结构更改收到通知。 您需要添加自己的过滤器以查找您感兴趣的特定视图。

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);
}

使用方法如下:

InstallAddSubviewListener(^(id _self, UIView *subview) {
    NSLog(@"-[UIView didAddSubview:]   self=%@, view=%@", _self, subview);
});

2
既然没有理智的答案可能出现,而且那个建议太丑不值得一些奖励,我将接受这个回答。 - Mattia
1
它太丑了,所以它很美。 - danh

2

基于@doug-richardson的代码,为什么不使用更简洁的代码来实现superview属性的KVO呢?

//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
你测试过了吗?我不确定superview会在你调用didChangeValueForKey:@"superview"之前被更新为新值。 - Doug Richardson
这是错误的使用方式,但接近一个好主意。问题在于“superview”在willChange和didChange调用之间不会改变(当您调用didChangeValueForKey时尚未设置)。您可能需要使用块覆盖两个willChange,并为didChange使用单独的块和IMP。 - dbquarrel

2
这是我的解决方案,结合以上的想法,修复了一些错误,并使其具有可扩展性。你可以直接使用它来监视某个类及其子类的superview变化,这应该是即插即用的。
用C语言编写以便易于理解和快速。你可以将其修改为NSObject的一些漂亮分类。
示例用法:
add_superview_kvo(UILabel.class);

那么您需要像正常使用一样向实例中添加自己的观察者。"Original Answer" 翻译成 "最初的回答"。
// 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];
    });
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接