How do you tell if a key exists for an object using Key-Value Coding?
Asked Answered
E

4

47

I'd like to test whether an object has a writeable @property in the iPhone SDK.

One possible way of doing this is to check the -valueForKey: method, but that seems rather inelegant!

Example:

  @try {
    id *value = [instance valueForKey:@"myProperty"];
  }
  @catch (NSException * e) {
    // Key did not exist
  }

Is there a better way of doing this?

Endgame answered 8/12, 2009 at 17:55 Comment(1)
P
60

If you are creating the object that is being checked, you could override valueForUndefinedKey: and setValue:forUndefinedKey to do something more useful than raising an exception.

If, on the other hand, you are trying to introspect objects you don't know about at runtime, you will have to use the runtime methods to do that. You can either use the objective-c runtime itself and call either class_copyPropertyList or protocol_copyPropertyList and deal with those, or use Foundation and call respondsToSelector on the object for the KVC getter/setters for a given property, e.g., for a property foo you would call something like [someObject respondsToSelector:NSSelectorFromString(@"foo")];.

Phytology answered 8/12, 2009 at 18:3 Comment(0)
H
7

There is no way, in general, to determine if an object has a value for a given key. An instance may decide to return a value for an otherwise undefined key from its -valueForUndefinedKey: method. Or it may let the default implementation throw an exception. Modern Objective-C (2.0) objects often declare relevant properties in their public API. For these objects, you can use the Objective-C runtime's class_copyPropertyList or protocol_copyPropertyList to get a list of the available properties. You can also emulate the KVC search list, testing via repondsToSelector: if the instance responds to an appropriate getter method. Finally you could use the runtime's class_copyIvarList to check the ivars for an appropriate ivar. This is a lot of work that you shouldn't be doing. It won't guarantee that you know whether an object instance will raise an exception when sent a valueForKey: message and it indicates a bigger issue...

If you have a collection of objects that must all provide a value for a given key, but some don't, you have a design problem. You should define an appropriate interface (a @protocol in Objective-C) that declares the needed properties. You can then test for conformance to this protocol, or make use of the compiler to do some compile-time type checking for you to make sure instance conform to the protocol (if you're passing around single instances).

Hammerlock answered 8/12, 2009 at 18:26 Comment(3)
Perhaps it would help to clarify! The application runs on multiple runtimes, with different versions of libraries available. In one version, a property I need to set (of primitive type) doesn't exist - hence, the requirement to check if it exists.Endgame
If you control the classes in question and so know that the class will respond to an appropriate selector in the library version that includes your property, use respondsToSelector:. In this case, you should use the getter method directly instead of valueForKey: since it will be faster.Hammerlock
Unfortunately, I don't control the classes in question. To make things easier, I've switched to using NSInvocation to set the primitively-typed property instead of using Key-Value Coding (and am indeed using respondsToSelector:).Endgame
P
2

Following on from @Jason Coco answer.

Here is my offering on how to check specifically for the "set" selectors existence of the property you're trying to set.

This is pure filth but it works and I've found myself using it. You've been warned..

NS_INLINE SEL _Nonnull IBPropertySetSelectorForPropertyName(NSString*_Nonnull propertyName)
{

    NSString* firstLetter = [propertyName substringToIndex:1];
    propertyName = [propertyName substringFromIndex:1];

    propertyName = [[NSString alloc] initWithFormat:@"set%@%@:",firstLetter.capitalizedString,propertyName];

    return NSSelectorFromString(propertyName);
}

Usage:

In swift: Add the code snippet to your bridging header then:

let key = "someKey"    
let sel = IBPropertySetSelectorForPropertyName(key)

if SOMETHING.responds(to: sel) {SOMETHING.setValue(value, forKeyPath: key)}
Pandurate answered 16/2, 2018 at 18:59 Comment(2)
Why not just use NSSelectorFromString("setPropertyName:") in Swift?Eppes
Only Selector("selectorName:") generates a warning. NSSelectorFromString(...) does not. Obviously it's not safer in any way.Eppes
C
0

The try/catch approach you propose in your question is the only reliable way of knowing whether valueForKey: will throw an exception.

  @try {
    id *value = [instance valueForKey:@"myProperty"];
  }
  @catch (NSException * e) {
    // Key did not exist
  }
Calvano answered 23/7, 2020 at 2:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.