iOS: как узнать, соответствует ли объект KVO?

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

В частности, я хотел бы знать, соответствует ли @property(nonatomic,retain) UIViewController *rootViewController из 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

person ma11hew28    schedule 07.07.2011    source источник


Ответы (4)


Краткий ответ: Нет.

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

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


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

person Dave DeLong    schedule 07.07.2011
comment
Хорошо, спасибо! Итак, в качестве дополнительного вопроса, выглядит ли код правильно и хорошо? Я впервые добавляю ivar через категорию. - person ma11hew28; 07.07.2011
comment
Хм ... Ага, скомпилировать для устройства iPhone 4.3.1 не удалось. Я получил 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. Итак, я думаю, что я сделаю подкласс для iOS 3.2. Еще немного кода, но этого должно хватить! Спасибо! - person ma11hew28; 07.07.2011
comment
@MattDiPasquale только что добавил пример того, как может выглядеть ваш подкласс. - person Dave DeLong; 07.07.2011
comment
Благодарность! Я пошел с подклассом, как вы предложили. Но как насчет использования ассоциативных ссылок с категория? - person ma11hew28; 07.07.2011
comment
Спасибо за это. Это мило! :) Как мне переадресовать декларацию собственности? Я знаю, что могу просто сделать: [super performSelector:@selector(rootViewController)] и [super performSelector:@selector(setRootViewController:) withObject: rootViewController] - person ma11hew28; 08.07.2011
comment
@DaveDeLong, позвольте нам продолжить обсуждение в чате - person ma11hew28; 08.07.2011

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

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

@end
person ma11hew28    schedule 08.07.2011
comment
Это все еще плохая идея. Категории в объектах Apple Framework всегда должны иметь префиксы к своим именам методов, чтобы избежать конфликтов с будущими или частными методами. - person uchuugaka; 14.02.2015

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

Основываясь на отзывах @David DeLong, я выбрал такой простой подкласс:

// UIWindow3.h

#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONErootViewController0
@interface UIWindow3 : UIWindow {

}

@property (nonatomic, retain) UIViewController *rootViewController;

@end
#endif

// UIWindow3.m

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

person ma11hew28    schedule 07.07.2011
comment
У вас все еще есть проблемы с iOS 5, о которых я упоминал ранее. Вы не можете решить эту проблему с помощью проверки во время компиляции; это должна быть проверка во время выполнения. - person Dave DeLong; 07.07.2011