18

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 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
Cœur
  • 37,241
  • 25
  • 195
  • 267
ma11hew28
  • 121,420
  • 116
  • 450
  • 651

4 Answers4

21

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

Dave DeLong
  • 242,470
  • 58
  • 448
  • 498
  • Cool, thanks! So then, as a follow-up question, does the code look correct & good? This is the first time I'm adding an ivar via a category. – ma11hew28 Jul 07 '11 at 14:54
  • Hmm... Yeah, I couldn't compile it for iPhone 4.3.1 device. I got `Undefined symbols for architecture armv6: "_OBJC_IVAR_$_UIWindow._rootViewController", referenced from: -[UIWindow(Additions) setRootViewController:] in UIWindow+Additions.o ld: symbol(s) not found for architecture armv6 collect2: ld returned 1 exit status`. So, I'll subclass it for iOS 3.2 I guess. A little more code, but that should do it! Thanks! – ma11hew28 Jul 07 '11 at 15:07
  • @MattDiPasquale just added an example of what your subclass might look like. – Dave DeLong Jul 07 '11 at 15:11
  • thanks! I went with a subclass as you suggested. But, what about using [Associative References with a category](http://developer.apple.com/library/ios/#documentation/Cocoa/Conceptual/ObjectiveC/Chapters/ocAssociativeReferences.html)? – ma11hew28 Jul 07 '11 at 18:08
  • thanks for this. This is sweet! :) How do I forward declare a property? I know I could just do: `[super performSelector:@selector(rootViewController)]` and `[super performSelector:@selector(setRootViewController:) withObject: rootViewController]` – ma11hew28 Jul 07 '11 at 22:58
  • @DaveDeLong let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/1246/discussion-between-mattdipasquale-and-dave-delong) – ma11hew28 Jul 07 '11 at 22:59
0

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
Community
  • 1
  • 1
ma11hew28
  • 121,420
  • 116
  • 450
  • 651
  • This is still a bad idea. Categories on Apple Framework objects should always prefix their method names to avoid clashes with future or private methods. – uchuugaka Feb 14 '15 at 05:39
-1

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
Community
  • 1
  • 1
ma11hew28
  • 121,420
  • 116
  • 450
  • 651
-2

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.

ma11hew28
  • 121,420
  • 116
  • 450
  • 651
  • 1
    You still have the iOS 5 problems I mentioned previously. You can't solve that problem with a compile-time check; it has to be a run-time check. – Dave DeLong Jul 07 '11 at 18:28