IOS: Как узнать, соответствует ли свойство KVO?

В Руководстве по наблюдению за ключевыми значениями раздел Регистрация для наблюдения за ключевыми значениями говорит: Обычно свойства в поставляемых Apple фреймворках являются только KVO-совместимыми, если они документируются как таковые ". Но я не нашел никаких свойств в документации, которые задокументированы как KVO-совместимые. Не могли бы вы указать мне некоторые?

В частности, я хотел бы знать, соответствует ли @property(nonatomic,retain) UIViewController *rootViewController of UIWindow KVO-совместимым. Причина в том, что я добавляю свойство rootViewController к UIWindow для iOS < 4 и хочу знать, должен ли я сделать это KVO-совместимым.

@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

Ответ 1

Короткий ответ: No.

Длинный ответ: ничто в UIKit не гарантирует соответствия KVO. Если вы обнаружите, что KVO-ing собственности работает, будьте благодарны, это непреднамеренно. Также: будьте осторожны. В будущем это может сильно разорваться.

Если вы обнаружите, что это то, что вам нужно, напишите запрос на улучшение.


О вашем реальном коде, он по своей сути ошибочен. НЕ пытайтесь добавить установщик "rootViewController" в UIWindow таким образом. Он будет разбиваться, когда вы компилируете свой код на iOS 4, но кто-то запускает его на устройстве iOS 5. Поскольку вы скомпилировали с использованием SDK 4.x, операторы #if будут оценивать значение true, что означает, что ваш smashher метода категории будет включен в двоичный файл. Однако, когда вы запускаете его на устройстве 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
  }
}

Caveat Implementor: я набрал это в окне браузера

Ответ 2

Основываясь на @David DeLong решении, это то, что я придумал, и он прекрасно работает.

В принципе, я сделал категорию на UIWindow. И в +load, я (время выполнения) проверяет, есть ли [UIWindow instancesRespondToSelector:@selector(rootViewController)]. Если нет, я использую class_addMethod() для динамического добавления методов getter и setter для rootViewController. Кроме того, я использую objc_getAssociatedObject и objc_setAssociatedObject для получения и установки rootViewController в качестве переменной экземпляра 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, "[email protected]:@");
    }
}
#endif

@end

Ответ 3

Здесь используется решение с использованием Ассоциативные ссылки для определения переменной экземпляра с категорией. Но, это не работает, по словам @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

Ответ 4

Основываясь на обратной связи @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 < 4) просто добавьте rootViewController в UIWindow через категорию. Я могу изучить это с помощью category, используя Ассоциативные ссылки (только для iOS < 4), потому что я думаю, что похоже, что это будет наиболее красноречивое решение и может быть хорошей техникой для изучения и использования в панели инструментов.