iOS:如何知道属性是否符合KVO标准?

18
Key-Value Observing Programming Guide中,Registering for Key-Value Observing部分指出,“通常,仅当苹果提供的框架中的属性被记录为KVO-compliant时,它们才满足KVO特性。”但是,在文档中我没有找到任何被记录为KVO-compliant的属性。请问您能否指出一些这样的属性?
具体来说,我想知道UIWindow@property(nonatomic,retain) UIViewController *rootViewController是否是KVO-compliant。原因是我正在为iOS < 4添加rootViewController属性到UIWindow,想知道我是否应该使其成为KVO-compliant。
@interface UIWindow (Additions)

#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_4_0
@property (nonatomic, retain) UIViewController *rootViewController;
#endif;

@end

@implementation UIWindow (Additions)

#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_4_0
@dynamic rootViewController;

- (void)setRootViewController:(UIViewController *)newRootViewController {
    if (newRootViewController != _rootViewController) {
        // Remove old views before adding the new one.
        for (UIView *subview in [self subviews]) {
            [subview removeFromSuperview];
        }
        [_rootViewController release];
        _rootViewController = newRootViewController;
        [_rootViewController retain];
        [self addSubview:_rootViewController.view];
    }
}
#endif

@end
4个回答

21

简短回答:不行。

详细回答:在UIKit中没有任何一个东西保证是符合KVO的。如果你发现对属性进行KVO操作可以成功,那么你很幸运,但这并非有意为之。同时需要注意的是,这种做法可能在将来失效。

如果你确实需要这样的功能,请提交增强请求


至于你的代码,它本质上是有缺陷的。不要试图以这种方式向UIWindow添加“rootViewController”设置器,因为这会导致问题。当你在iOS 4上编译代码,而其他人在iOS 5设备上运行时,它失效。因为你使用了4.x SDK进行编译,所以#if语句将评估为true,也就是说,你的类别方法破坏程序将包含在二进制文件中。然而,当你在iOS 5设备上运行它时,你会得到一个方法冲突,因为UIWindow上的两个方法将具有相同的方法签名,但不能保证哪个方法会被使用

不要这样搞框架。如果你必须要这样做,那就使用子类化。这就是为什么有子类化存在的原因。


你的子类应该长这样:

@interface CustomWindow : UIWindow

@property (nonatomic, retain) UIViewController *rootViewController;

@end

@implementation CustomWindow : UIWindow

static BOOL UIWindowHasRootViewController = NO;

@dynamic rootViewController;

- (void)_findRootViewControllerMethod {
  static dispatch_once_t predicate;
  dispatch_once(&predicate, ^{
    IMP uiwindowMethod = [UIWindow instanceMethodForSelector:@selector(setRootViewController:)];
    IMP customWindowMethod = [CustomWindow instanceMethodForSelector:@selector(setRootViewController:)];
    UIWindowHasRootViewController = (uiwindowMethod != NULL && uiwindowMethod != customWindowMethod);
  });
}

- (UIViewController *)rootViewController {
  [self _findRootViewControllerMethod];
  if (UIWindowHasRootViewController) {
    // this will be a compile error unless you forward declare the property
    // i'll leave as an exercise to the reader ;)
    return [super rootViewController];
  }
  // return the one here on your subclass
}

- (void)setRootViewController:(UIViewController *)rootViewController {
  [self _findRootViewControllerMethod];
  if (UIWindowHasRootViewController) {
    // this will be a compile error unless you forward declare the property
    // i'll leave as an exercise to the reader ;)
    [super setRootViewController:rootViewController];
  } else {
    // set the one here on your subclass
  }
}

请注意实现者:我是在浏览器窗口中输入的


很酷,谢谢!那么,作为后续问题,这段代码看起来正确和好吗?这是我第一次通过分类添加一个ivar。 - ma11hew28
嗯...是的,我无法将其编译为iPhone 4.3.1设备。我得到了“Undefined symbols for architecture armv6: "OBJC_IVAR$_UIWindow._rootViewController",引用自:-[UIWindow(Additions) setRootViewController:] in UIWindow+Additions.o ld: symbol(s) not found for architecture armv6 collect2: ld returned 1 exit status”。所以,我猜我要为iOS 3.2子类化它。多写一点代码,但应该可以解决问题!谢谢! - ma11hew28
@MattDiPasquale 刚刚添加了一个示例,展示了你的子类可能会是什么样子。 - Dave DeLong
谢谢!我按照你的建议使用了子类。但是,使用带有类别的关联引用怎么样? - ma11hew28
谢谢这个。太棒了! :) 如何前向声明一个属性?我知道我可以直接用[super performSelector:@selector(rootViewController)][super performSelector:@selector(setRootViewController:) withObject: rootViewController] - ma11hew28
@DaveDeLong 让我们在聊天室里继续这个讨论 - ma11hew28

0

基于 @David DeLong's solution,我得出了下面的解决方案,并且它非常有效。

基本上,我在UIWindow上创建了一个分类。然后在+load中,我(运行时)检查是否存在[UIWindow instancesRespondToSelector:@selector(rootViewController)]。 如果不存在,则使用class_addMethod()来动态添加rootViewController的getter和setter方法。此外,我使用objc_getAssociatedObjectobjc_setAssociatedObjectrootViewController作为UIWindow的实例变量获取并设置。

// UIWindow+Additions.h

@interface UIWindow (Additions)

@end

// UIWindow+Additions.m

#import "UIWindow+Additions.h"
#include <objc/runtime.h>

@implementation UIWindow (Additions)

#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_4_0
// Add rootViewController getter & setter.
static UIViewController *rootViewControllerKey;

UIViewController *rootViewController3(id self, SEL _cmd);
void setRootViewController3(id self, SEL _cmd, UIViewController *newRootViewController);

UIViewController *rootViewController3(id self, SEL _cmd) {
    return (UIViewController *)objc_getAssociatedObject(self, &rootViewControllerKey);
}

void setRootViewController3(id self, SEL _cmd, UIViewController *newRootViewController) {
    UIViewController *rootViewController = [self performSelector:@selector(rootViewController)];
    if (newRootViewController != rootViewController) {
        // Remove old views before adding the new one.
        for (UIView *subview in [self subviews]) {
            [subview removeFromSuperview];
        }
        objc_setAssociatedObject(self, &rootViewControllerKey, newRootViewController,
                                 OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        [self addSubview:newRootViewController.view];
    }
}

+ (void)load {
    if (![UIWindow instancesRespondToSelector:@selector(rootViewController)]) {
        class_addMethod([self class], @selector(rootViewController),
                        (IMP)rootViewController3, "@@:");
        class_addMethod([self class], @selector(setRootViewController:),
                        (IMP)setRootViewController3, "v@:@");
    }
}
#endif

@end

这仍然是一个不好的想法。苹果框架对象上的类别应始终在其方法名称前加上前缀,以避免与将来或私有方法的冲突。 - uchuugaka

-1

这里有一个使用关联引用来定义带类别的实例变量的解决方案。但是,它不起作用,因为根据@Dave DeLong的说法,我必须对此进行运行时(而不是编译时)检查

// UIWindow+Additions.h

@interface UIWindow (Addtions)

#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_4_0
@property (retain, nonatomic) UIViewController *rootViewController;
#endif

@end

// UIWindow+Additions.m

#import "UIWindow+Additions.h"
#include <objc/runtime.h>

@implementation UIWindow (Additions)

#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_4_0
@dynamic rootViewController;

static UIViewController *rootViewControllerKey;

- (UIViewController *)rootViewController {
    return (UIViewController *)objc_getAssociatedObject(self, &rootViewControllerKey);
}

- (void)setRootViewController:(UIViewController *)newRootViewController {
    UIViewController *rootViewController = self.rootViewController;
    if (newRootViewController != rootViewController) {
        // Remove old views before adding the new one.
        for (UIView *subview in [self subviews]) {
            [subview removeFromSuperview];
        }
        [rootViewController release];
        objc_setAssociatedObject(self, &rootViewControllerKey, newRootViewController,
                                 OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        [rootViewController retain];
        [self addSubview:rootViewController.view];
    }
}
#endif

@end

-2

根据 @David DeLong 的反馈,我采用了一个简单的子类,如下所示:

// UIWindow3.h

#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_4_0
@interface UIWindow3 : UIWindow {

}

@property (nonatomic, retain) UIViewController *rootViewController;

@end
#endif

// UIWindow3.m

#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_4_0
#import "UIWindow3.h"

@implementation UIWindow3

@synthesize rootViewController;

- (void)setRootViewController:(UIViewController *)newRootViewController {
    if (newRootViewController != rootViewController) {
        // Remove old views before adding the new one.
        for (UIView *subview in [self subviews]) {
            [subview removeFromSuperview];
        }
        [rootViewController release];
        rootViewController = newRootViewController;
        [rootViewController retain];
        [self addSubview:rootViewController.view];
    }
}

@end
#endif

然而,这也需要浏览现有代码并使用条件编译将UIWindow转换为UIWindow3,无论何时访问rootViewController。(注意:我认为@David DeLong的解决方案可能不需要进行这些额外的更改,而只需始终使用CustomWindow而不是UIWindow。)因此,这比如果我可以(仅适用于iOS&lt;4),通过类别将rootViewController添加到UIWindow要烦人得多。我可能会尝试使用使用关联引用的类别(仅适用于iOS&lt;4),因为我认为这看起来是最优雅的解决方案,并且可能是一个很好的学习和掌握的技巧。


1
你仍然有我之前提到的iOS 5问题。你不能用编译时检查解决这个问题,必须是运行时检查。 - Dave DeLong

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