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

前端 未结 4 1053
失恋的感觉
失恋的感觉 2020-11-30 07:36

In the Key-Value Observing Programming Guide, the section Registering for Key-Value Observing says \"Typically properties in Apple-supplied frameworks are only KVO-compliant

相关标签:
4条回答
  • 2020-11-30 08:11

    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 讨论(0)
  • 2020-11-30 08:16

    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
    
    0 讨论(0)
  • 2020-11-30 08:20

    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

    0 讨论(0)
  • 2020-11-30 08:31

    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
    
    0 讨论(0)
提交回复
热议问题