开发者

iOS: How do I know if a property is KVO-compliant?

In the Key-Value Observing Programming Guide, the section Registering开发者_如何学Python for Key-Value Observing says "Typically properties in Apple-supplied frameworks are only KVO-compliant if they are documented as such." But, I haven't found any properties in the documentation that are documented as KVO-compliant. Would you please point me to some?

Specifically, I would like to know if the @property(nonatomic,retain) UIViewController *rootViewController of UIWindow is KVO-compliant. The reason is that I'm adding the rootViewController property to UIWindow for iOS < 4 and want to know if I should make it 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


Short answer: No.

Long answer: Nothing in UIKit is guaranteed to be KVO-compliant. If you happen to find that KVO-ing a property works, be grateful, it's unintentional. Also: be wary. It could very well break in the future.

If you find that this is something you need, please file an enhancement request.


About your actual code, it's inherently flawed. Do NOT attempt to add a "rootViewController" setter to UIWindow this way. It will break when you compile your code on iOS 4 but someone runs it on an iOS 5 device. Because you compiled using the 4.x SDK, the #if statements will evaluate to true, meaning your category method smasher will be included in the binary. However, when you run it on an iOS 5 device, you're now going to get a method conflict because two methods on UIWindow will have the same method signature, and there's no guarantee as to which one will be used.

Don't screw with the frameworks like this. If you have to have this, use a subclass. THIS IS WHY SUBCLASSING EXISTS.


Your subclass would look something like this:

@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
  }
}

Caveat Implementor: I typed this in a browser window


Based on @David DeLong's solution, this is what I came up with, and it works beautifully.

Basically, I made a category on UIWindow. And in +load, I (run-time) check whether [UIWindow instancesRespondToSelector:@selector(rootViewController)]. If not, I use class_addMethod() to dynamically add the getter & setter methods for rootViewController. Also, I use objc_getAssociatedObject and objc_setAssociatedObject to get & set the rootViewController as an instance variable of 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


Here's a solution using Associative References to define an instance variable with a category. But, it doesn't work cause, according to @Dave DeLong, I must use a run-time (not compile-time) check for this.

// 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


Based on @David DeLong's feedback, I went with a simple subclass like so:

// 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

However, this also required going through the existing code and using conditional compilation to cast UIWindow to UIWindow3 where ever rootViewController was being accessed. (Note: I think @David DeLong's solution may not require making these additional changes but rather just always using CustomWindow instead of UIWindow.) Thus, this is more annoying than if I could (only for iOS < 4) just add the rootViewController to UIWindow via a category. I may look into doing this with a category using Associative References (only for iOS < 4) because I think that looks like it'd be the most eloquent solution and might be a good technique to learn and have in the toolbox.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜