Checking If GKScore Instances Have Context Property - objective-c

For the online mode for my game, I am using the context property of GKScore, and as all devices which support Game Center can update to iOS 5 (which is when the context property was added), I am requiring that the context property is available to play online. However, I am having issues implementing this runtime-check. I was assuming that I could use [GKScore instancesRespondToSelector:#selector(setContext:)] to check its existence, but this returns false on the iOS 5 and 5.1 simulators, as well as for #selector(context). Why on earth is this happening, and what is the cleanest and correct way to perform this check, please?

This looks like a bug in the GK implementation.
Consider the following code...
// Get the C-functions that are really called when the selector message is sent...
typedef BOOL (*XX)(id, SEL, SEL);
XX classImpNSObject = (XX)[NSObject
methodForSelector:#selector(instancesRespondToSelector:)];
XX classImpGKScore = (XX)[GKScore
methodForSelector:#selector(instancesRespondToSelector:)];
XX instImpNSObject = (XX)[NSObject
instanceMethodForSelector:#selector(respondsToSelector:)];
XX instImpGKScore = (XX)[GKScore
instanceMethodForSelector:#selector(respondsToSelector:)];
// See that the same C function is called for both of these...
NSLog(#"instancesRespondToSelector: %p, %p", classImpNSObject, classImpGKScore);
// But, different functions are called for these...
NSLog(#"respondsToSelector: %p, %p", instImpNSObject, instImpGKScore);
// Invoke to C-Functions for instancesRespondToSelector:
NSLog(#"NSObject instancesRespondToSelector: context: %s",
classImpNSObject(
[NSObject class],
#selector(instancesRespondToSelector:),
#selector(context))
? "YES" : "NO");
NSLog(#"GKScore instancesRespondToSelector: context: %s",
classImpGKScore(
[GKScore class],
#selector(instancesRespondToSelector:),
#selector(context))
? "YES" : "NO");
// Invoke the C functions for respondsToSelector:
GKScore *gkScore = [[GKScore alloc] init];
NSLog(#"NSObject respondsToSelector: context: %s",
instImpNSObject(
gkScore,
#selector(respondsToSelector:),
#selector(context))
? "YES" : "NO");
NSLog(#"GKScore respondsToSelector: context: %s",
instImpGKScore(
gkScore,
#selector(respondsToSelector:),
#selector(context))
? "YES" : "NO");
Basically, we just extracted the C functions that get called when responding to those messages.
As you can see, NSObject and GKScore use the exact same C-function implementation for instancesRespondToSelector:. However, they use different C-function implementations for respondsToSelector:. This means that GKScore overrides respondsToSelector: with its own implementation (but does not override instancesRespondToSelector.
If you send the same GKScore instance to the different C implementations of respondsToSelector: you get different results for some selectors (obviously, or there would not be a reason to provide a subclass implementation).
It looks like they did something funky for a few special properties, and provided an override for respondsToSelector: to handle the special cases, but forgot about making sure instancesRespondToSelector: did the right thing.
If you want to troll through assembly code, set a breakpoint and I'm sure you can see the differences.
I did not do that.
My own personal curiosity will only carry me so far :-)
For you situation, of trying to detect the method implementation in code, I suggest creating a temporary GKScore object to do your tests, cache that result, and free the temporary object.

I can't fully explain this, but an instantiated object of class GKScore will return YES to repondsToSelector(context), even while the class says it won't. If no other solution works, construct a GKScore object just to query it.
I wondered if [[GKScore alloc] init] actually returns an object with type other than GKScore. This can happen.
GKScore *instantiatedScore = [[GKScore alloc] init]; // Add autorelease if using manual reference counting.
NSString* className = NSStringFromClass([instantiatedScore class]);
NSLog(#"instantiatedScore class name = %#", className);
But, it doesn't, according to this output:
instantiatedScore class name = GKScore
I wondered if the compiler directives in the GKSCore.h header file might affect this. It defines two properties that are only available in iOS 5.0 or greater: context and shouldSetDefaultLeaderboard. Maybe those compiler directives mean that the class can't guarantee it will support those two properties.
Under this hypothesis [GKScore instancesRepondToSelector:#selector(category)] should return YES, but [GKScore instancesRepondToSelector:#selector(shouldSetDefaultLeaderboard)] should return NO.
GKScore *instantiatedScore = [[GKScore alloc] init]; // Add autorelease if using manual reference counting.
NSLog(#"GKScore category = %d", [GKScore instancesRespondToSelector:#selector(category)]);
NSLog(#"instantiatedScore category = %d", [instantiatedScore respondsToSelector:#selector(category)]);
NSLog(#"GKScore context = %d", [GKScore instancesRespondToSelector:#selector(context)]);
NSLog(#"instantiatedScore context = %d", [instantiatedScore respondsToSelector:#selector(context)]);
NSLog(#"GKScore shouldSetDefaultLeaderboard = %d", [GKScore instancesRespondToSelector:#selector(shouldSetDefaultLeaderboard)]);
NSLog(#"instantiatedScore shouldSetDefaultLeaderboard = %d", [instantiatedScore respondsToSelector:#selector(shouldSetDefaultLeaderboard)]);
But, the output is weirder than that:
GKScore category = 0
instantiatedScore category = 1
GKScore context = 0
instantiatedScore context = 1
GKScore shouldSetDefaultLeaderboard = 1
instantiatedScore shouldSetDefaultLeaderboard = 1

If you're specifically looking for the existence of a property, you should use the Objective-C runtime function:
class_getProperty(Class cls, const char *name)
To use it, you will have to import :
#import <objc/runtime.h>
As a tiny test example, here is how you could test for the existence of a particular property:
#import <objc/runtime.h>
//...
objc_property_t realP = class_getProperty([GKScore class], "context");
objc_property_t fakeP = class_getProperty([GKScore class], "fakeContext");
if (realP) {
NSLog(#"context exists");
}
if (!fakeP) {
NSLog(#"fakeContext does not exist");
}
// Both statements will log correctly.
As to why GKScore instances do not appear to respond to the correct selector, my thought would be that the context property may be declared #dynamic and thus +instancesRespondToSelector: and -respondsToSelector: would return NO (see this question). Not knowing the internal details, this is all I can suggest, but if you merely want to test the existence of a property, the sample code above will work.
Incidentally, if you don't want an include to the Objective-C runtime floating around, you may want to encapsulate this behaviour in a class or wrap it in a selector rather than just stick it in somewhere verbatim. That's entirely up to you of course.

I have also come across this issue but in my case with GKTurnBasedMatchParticipant. I did a quick dump of the result of sending #instancesRespondToSelector: to each of the properties of this class.
Here's the result:
1 playerID false
2 lastTurnDate false
3 status true
4 matchOutcome false
5 matchOutcomeString true
6 isWinner true
7 invitedBy false
8 inviteMessage false
9 internal true
Notice how many of the properties report that they can't be sent as selectors. However, notice also an additional "internal" property. Now look at the result of querying whether this internal object will respond to the property selectors:
1 playerID true
2 lastTurnDate true
3 status true
4 matchOutcome true
5 matchOutcomeString false
6 isWinner false
7 invitedBy true
8 inviteMessage true
9 internal false
Hence, many of the missing properties are in here. I guess that it's not really safe to make use of a non-documented "internal" feature to get around an apparent Apple bug but it's still interesting to know nonetheless.
EDIT: After another day's mucking around I have found the issue here. These rogue properties are actually set up as forwarding methods to forward to the "internal" object. Being an ObjectiveC noob, I hadn't realized that this is a perfectly acceptable thing to do.
In my case, I'm not just trying to detect if an object responds to a selector but I actually want to invoke it too. Hence the general solution to cope with forwarding is:
(a) To check availability of a response use [instance #respondsToSelector: sel] rather than [[instance class] instanceRespondsToSelector: del].
(b) To invoke a method that may, or may not, be forwarded do this:
NSMethodSignature *signature = [instance methodSignatureForSelector:sel];
if (!signature) {
// It's possible this is a message forwarding selector, so try this before giving up.
NSObject *fwd=[instance forwardingTargetForSelector:sel];
if (fwd && (signature= [fwd methodSignatureForSelector:sel]))
// Redirect to the forwarding target
instance=fwd;
else {
// ERROR case - selector is really not supported
}
}
NSInvocation *invocation=[NSInvocation invocationWithMethodSignature:signature];
// Proceed with invocation setup
I hope this is useful to prevent others from wasting as much time as I have on this.

Related

Objective-C method parameter type-safety

If I have methods like:
- (BOOL)isValidRow:(NSDictionary*)contentVersionRow
do we really have to continually check like this at the beginning of the method
if(![contentVersionRow isKindOfClass:[NSDictionary class]]) {
// Handle unusual situation - probably return NO in this case
}
to really implement proper type-safety inside Objective-C methods? Because in theory the parameter is not guaranteed to point to an NSDictionary object, is this correct?
EDIT: So answers so far seem to indicate we should not check for this, but then what is the difference between checking for this and checking for nil parameter, which I assume we should do? Or should we not check for nil either, if it's not normally expected? Both cases cover the situation of a misbehaving caller.
Just like in C you are dealing with pointers in Objective-C. So saying NSDictionary * simply means "here's a pointer to a memory address that contains an instance of NSDictionary".
Example:
#import <Foundation/Foundation.h>
#interface Test : NSObject
- (void)useDictionary:(NSDictionary *)dictionary;
#end
#implementation Test
- (void)useDictionary:(NSDictionary *)dictionary
{
NSLog(#"Keys: %#", [dictionary allKeys]);
}
#end
int main(int argc, char *argv[]) {
#autoreleasepool {
Test *test = [[Test alloc] init];
// 1: This works fine
[test useDictionary:#{#"key": #"value"}];
// 2: This will cause a compiler warning (or error depending on the options passed to the compiler)
[test useDictionary:#"not a dictionary"];
// 3: This will compile without any warnings
[test useDictionary:(NSDictionary *)#"not a dictionary"];
}
}
The 2nd and 3rd examples will cause the program to crash at runtime. So if you want to pass incorrect things to methods, you can. Usually Xcode will warn you if you have a type-mismatch.
Update about nil-checking: If it's an API-misuse to pass nil to your method, then throw an exception. That's what exceptions are for in Objective-C: to catch programming mistakes, not to handle expected runtime issues (like an unreachable network). If your method can just silently fail if nil is passed in or handle it in a sensible way, then do that instead. For example if you have a method addValue:(NSNumber *)number that adds the given value to a sum then it wouldn't be a big deal if someone called it with nil: Just don't add anything :)
Yes, but you shouldn’t.
Obj-C is a dynamic language, so it is up to each object to determine if it responds to a certain method. It is bad style to check the class of an object.
Instead, if you want to check that an object supports a selector you should use -respondsToSelector:, but only if you handle objects not responding to that selector.

XPC not registering classes correctly for collection

I'm using XPC in one of my apps on 10.8. It's got the standard setup with protocols defined for exported interface and the remote interface. The problem I run into is with one of my methods on the exported interface.
I have a model class, lets just call it Foo. This class conforms to NSSecureCoding, implements +supportsSecureCoding, and encodes/decodes the internal properties correctly using the secure coding methods. When passing this object through a method on my exported interface that only involves a single instance, it works fine.
The problem occurs when I want to pass a collection of these objects, or a NSArray of Foo objects. Here's an example of what the signature on the exported interface looks like:
- (void)grabSomethingWithCompletion:(void (^)(NSArray *foos))completion;
And I've whitelisted the Foo class, as noted in the documentation:
NSSet *classes = [NSSet setWithObject:Foo.class];
[exportedInterface setClasses:classes forSelector:#selector(grabSomethingWithCompletion:) argumentIndex:0 ofReply:YES];
Now this should make it so that this array can be safely copied across the process and decoded on the other side. Unfortunately this doesn't seem to be working as expected.
When calling the method on the exported protocol, I receive an exception:
Warning: Exception caught during decoding of received reply to message
'grabSomethingWithCompletion:', dropping incoming message and
calling failure block.
Exception: Exception while decoding argument 1 of invocation:
return value: {v} void target: {#?} 0x0
(block) argument 1: {#} 0x0
Exception: value for key 'NS.objects' was of unexpected class
'Foo'. Allowed classes are '{(
NSNumber,
NSArray,
NSDictionary,
NSString,
NSDate,
NSData )}'.
This almost seems like it didn't even register the whitelisting I performed earlier. Any thoughts?
EDIT 2: It depends on where you've whitelisted Foo. It needs to be whitelisted from within whatever is calling grabSomethingWithCompletion:. For instance, if you have a service that implements and exposes:
- (void)takeThese:(NSArray *)bars reply:(void (^)(NSArray *foos))completion;
Then you need the service side to whitelist Bar for the incoming connection:
// Bar and whatever Bar contains.
NSSet *incomingClasses = [NSSet setWithObjects:[Bar class], [NSString class], nil];
NSXPCInterface *exposedInterface = [NSXPCInterface interfaceWithProtocol:#protocol(InYourFaceInterface)];
[exposedInterface setClasses:incomingClasses forSelector:#selector(takeThese:reply:) argumentIndex:0 ofReply:NO];
// The next line doesn't do anything.
[exposedInterface setClasses:incomingClasses forSelector:#selector(takeThese:reply:) argumentIndex:0 ofReply:YES];
xpcConnection.exposedInterface = exposedInterface;
That second section has to go on the other end of the connection, whatever is talking to your service:
NSSet *incomingClasses = [NSSet setWithObjects:[Foo class], [NSNumber class], nil];
NSXPCInterface *remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:#protocol(InYourFaceInterface)];
[remoteObjectInterface setClasses:incomingClasses forSelector:#selector(takeThese:reply:) argumentIndex:0 ofReply:YES];
xpcConnection.remoteObjectInterface = remoteObjectInterface;
In summary, whatever is receiving strange objects needs to be the one whitelisting the strange objects. Not sure if this was your problem, but I'm sure it will be somebody's.
EDIT: Now that I've been working with XPC for a while longer, I realize that my answer, while solving a problem, does not solve your problem. I've run into this now a couple different times and I'm still not sure how to fix it outside of implementing my own collection class, which is less than ideal.
Original Answer:
I know it has been quite some time since you asked this, but after a ton of searching with no one answering this question, I thought I'd post my answer for what was causing it (there may be other causes, but this fixed it for me).
In the class that conforms to NSSecureCoding, in the initWithCoder: method, you need to explicitly decode collections by passing in a set of all possible classes contained within the collection. The first two are standard examples of decoding, and the last one is decoding a collection:
if (self = [super init]) {
self.bar = [aDecoder decodeInt64ForKey:#"bar"];
self.baz = [aDecoder decodeObjectOfClass:[Baz class] forKey:#"baz"];
NSSet *possibleClasses = [NSSet setWithObjects:[Collection class], [Foo class], nil];
self.foo = [aDecoder decodeObjectOfClasses:possibleClasses forKey:#"foo"];
}
So if you collection is a set containing NSStrings, possible classes would be [NSSet class] and [NSString class].
I'm sure you've moved on from this problem, but maybe someone else needs this answer as much as I did.
I encountered this same problem, I had to explicitly whitelist NSArray* as well
NSSet *classes = [NSSet setWithObjects: [Foo class], [NSArray class], nil];
Which is a bit counterintuitive since the documentation does not mention this requirement.
Actually it seems you need to add your custom class to the already whitelisted ones :
NSSet currentClasses = [remoteObjectInterface classesForSelector:#selector(takeThese:reply:) argumentIndex:0 ofReply:YES];
NSSet *allIncomingClasses = [currentClasses setByAddingObjectsFromSet:[NSSet setWithObjects:[Foo class], [NSNumber class], nil];
NSXPCInterface *remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:#protocol(InYourFaceInterface)];
[remoteObjectInterface setClasses:allIncomingClasses forSelector:#selector(takeThese:reply:) argumentIndex:0 ofReply:YES];
xpcConnection.remoteObjectInterface = remoteObjectInterface;

How to set expectations on parameters to mocked methods in Kiwi

Using OCMockito and OCHamcrest, I can set up expectations on the arguments to mocked methods, thusly:
[verify(aMockObject) doSomething:allOf(is(instanceOf([NSArray class])), hasCountOf(3U), nil)];
There doesn't seem to be an equivalently simple way to do this using Kiwi. It is possible to capture arguments using a spy, something like:
KWCaptureSpy *spy = [aMockObject captureArgument:#selector(doSomething:) atIndex:0];
NSArray *capturedArray = spy.argument;
And then to check expectations on the captured object:
[[capturedArray should] haveCountOf:3U];
Is there a less clumsy way to do this in Kiwi?
(I'm aware I could probably use hamcrest matchers in here, but for the moment I'm exploring what Kiwi is capable of).
One option that I have used is stub:withBlock:
NSArray* capturedArray; // declare this as __block if needed
[aMockObject stub:#selector(doSomething:)
withBlock:^id(NSArray *params) {
capturedArray = params[0];
// this is necessary even if the doSomething method returns void
return nil;
}];
// exercise your object under test, then:
[[capturedArray should] haveCountOf:3U];
This works fine, and I find it easier to implement than the spy pattern. But your question made me wonder about expectations using message patterns. For example:
[[[aMockObject should] receive] doSomething:myArray];
[[[aMockObject should] receive] doSomething:any()];
The first example will verify that aMockObject received the doSomething: message with an argument that isEqual:myArray. The second example will simply verify that doSomething: was sent, with no expectation about the array arugment. It would be great if we can specify some type of Matcher in the message pattern, to express that we don't care what specific array instance is sent in the message, just that it has a count of 3.
I haven't found any examples of being able to do this, but it looks like there are some possibilities. To verify a message-sending expectation, Kiwi uses the KWMessagePattern class, specifically the matchesInvocation: and argumentFiltersMatchInvocationArguments: methods. This checks for three types of "argument filters":
Literal object values (such as myArray in the example above), which are compared to the actual value sent in the message using isEqual:
An object of type KWAny (such as the any() macro in the example above), which will match any argument value
Objects that satisfy [KWGenericMatchEvaluator isGenericMatcher:argumentFilter], which basically means that the object responds to matches:(id)obj
Thus, you should be able to use objects that implement matches: in message-pattern expectations to do things like verify the length of arrays sent to stubbed methods, without resorting to spys or blocks. Here's a very simple implementation: (available as a Gist)
// A reusable class that satisfies isGenericMatcher:
#interface SOHaveCountOfGenericMatcher : NSObject
- (id)initWithCount:(NSUInteger)count;
- (BOOL)matches:(id)item; // this is what KWMessagePattern looks for
#property (readonly, nonatomic) NSUInteger count;
#end
#implementation SOHaveCountOfGenericMatcher
- (id)initWithCount:(NSUInteger)count
{
if (self = [super init]) {
_count = count;
}
return self;
}
- (BOOL)matches:(id)item
{
if (![item respondsToSelector:#selector(count)])
return NO;
return [item count] == self.count;
}
#end
// Your spec:
it(#"should receive an array with count 3", ^{
NSArray* testArray = #[#"a", #"b", #"c"];
id argWithCount3 = [[SOHaveCountOfGenericMatcher alloc] initWithCount:3];
id aMockObject = [SomeObj nullMock];
[[[aMockObject should] receive] doSomething:argWithCount3];
[aMockObject doSomething:testArray];
});
It would be nice to be able to reuse Kiwi's built-in matcher classes here, but I haven't yet found out exactly how to do this.

Using objc_msgSendSuper to invoke a class method

I was going through and replacing #synthesized(self) locks w/ this method
void _ThreadsafeInit(Class theClassToInit, void *volatile *theVariableItLivesIn, void(^InitBlock)(void))
{
//this is what super does :X
struct objc_super mySuper = {
.receiver = (id)theClassToInit,
.super_class = class_getSuperclass(theClassToInit)
};
id (*objc_superAllocTyped)(struct objc_super *, SEL, NSZone *) = (void *)&objc_msgSendSuper;
// id (*objc_superAllocTyped)(id objc_super, SEL, NSZone *) = (void *)&objc_msgSend;
do {
id temp = [(*objc_superAllocTyped)(&mySuper /*theClassToInit*/, #selector(allocWithZone:), NULL) init];//get superclass in case alloc is blocked in this class;
if(OSAtomicCompareAndSwapPtrBarrier(0x0, temp, theVariableItLivesIn)) { //atomic operation forces synchronization
if( InitBlock != NULL ) {
InitBlock(); //only the thread that succesfully set sharedInstance pointer gets here
}
break;
}
else
{
[temp release]; //any thread that fails to set sharedInstance needs to clean up after itself
}
} while (*theVariableItLivesIn == NULL);
}
which while a bit more verbose exhibits significantly better performance in non-contested cases
along with this little macro (excuse poor formatting, it's very simple). To allow the block to be declared after the initial nil check, looks to help LLVM keep the "already initialized" path extremely fast. That's the only one I care about.
#define ThreadsafeFastInit(theClassToInit, theVariableToStoreItIn, aVoidBlockToRunAfterInit) if( theVariableToStoreItIn == nil) { _ThreadsafeInitWithBlock(theClassToInit, (void *)&theVariableToStoreItIn, aVoidBlockToRunAfterInit); }
So initially implemented it using the commented out sections for objc_superAllocTyped (actually first using [theClassToInit allocWithZone:NULL], which was definitely the best approach :) ), which worked great until I realized that most of the singletons in the project had overridden allocWithZone to return the singleton method... infinite loop. So I figured using objc_msgSendSuper should sort it out quickly, but I get this error.
[51431:17c03] +[DataUtils allocWithZone:]: unrecognized selector sent to class 0x4f9584
The error doesn't seem to be related to the actual problem, as...
(lldb) po 0x4f9584
$1 = 5215620 DataUtils
(lldb) print (BOOL)[$1 respondsToSelector:#selector(allocWithZone:)]
(BOOL) $2 = YES
So I'm definitely missing something... I compared to assembly generated by a [super allocWithZone:NULL] method in an empty class... almost identical except for the functions called have different names (maybe just using different symbols, no idea, can't read it that well).
Any ideas? I can use class_getClassMethod on the superclass and call the IMP directly, but I'm trying to be reasonable in my abuse of the runtime :)
Alright, this wasn't actually that tricky once I recalled that the meta class contains all of the method information for a Class instance obtained via -[self class] or +[self] -> thanks http://www.cocoawithlove.com/2010/01/what-is-meta-class-in-objective-c.html
This error occurred because I was asking the runtime to look up the method in NSObject's set of instance methods, which obviously doesn't contain allocWithZone: . The mistake in the error log presumably originated because the receiver was a metaclass instance, and Apple has their interns implement error logs.
so while with a normal instance method call via objc_msgSendSuper, you would pass a metaclass instance as objc_super.super_class, to invoke a class method, the metaclass itself is needed (everything is one level up).
Example, and a diagram that helped me understand this - (http://www.sealiesoftware.com/blog/archive/2009/04/14/objc_explain_Classes_and_metaclasses.html)
struct objc_super mySuper;
mySuper.receiver = theClassToInit; //this is our receiver, no doubt about it
//either grab the super class and get its metaclass
mySuper.super_class = object_getClass( class_getSuperclass( theClassToInit ) );
//or grab the metaclass, and get its super class, this is the exact same object
mySuper.super_class = class_getSuperclass( object_getClass( theClassToInit ) );
Then the message can be resolved correctly. Makes perfect sense now that I started paying attention :P
Anyways, now that I found my mistake I feel like I've leveled up my Objc runtime understanding. I was also able to fix an architectural mistake made two years ago by someone I never met without having to modifying and re-test dozens of classes across 3 projects and 2 static libraries (God I love Objective-C). Replacing the #synchronized construct with a simple function call also halved the compiled code size of those methods. As a bonus, all our singleton accessors are now (more) threadsafe, because the performance cost for doing so is now negligible. Methods which naively re-fetched the singleton object multiple times (or in loops) have seen a huge speedup now that they don't have to acquire and release a mutex multiple times per invocation. All in all I'm very happy it all worked as I'd hoped.
I made a "normal" Objective-C method for this on a category of NSObject, which will work for both instance and Class objects to allow you to invoke a superclass's implementation of a message externally. Warning: This is only for fun, or unit tests, or swizzled methods, or maybe a really cool game.
#implementation NSObject (Convenience)
-(id)performSelector:(SEL)selector asClass:(Class)class
{
struct objc_super mySuper = {
.receiver = self,
.super_class = class_isMetaClass(object_getClass(self)) //check if we are an instance or Class
? object_getClass(class) //if we are a Class, we need to send our metaclass (our Class's Class)
: class //if we are an instance, we need to send our Class (which we already have)
};
id (*objc_superAllocTyped)(struct objc_super *, SEL) = (void *)&objc_msgSendSuper; //cast our pointer so the compiler can sort out the ABI
return (*objc_superAllocTyped)(&mySuper, selector);
}
so
[self performSelector:#selector(dealloc) asClass:[self superclass]];
would be equivalent to
[super dealloc];
Carry on runtime explorers! Don't let the naysayers drag you into their land of handwaving and black magik boxes, it's hard to make uncompromisingly awesome programs there*.
*Please enjoy the Objective-C runtime responsibly. Consult with your QA team for any bugs lasting more than four hours.

Objective-C & KeyValueCoding: How to avoid an exception with valueForKeyPath:?

I've got an object of type id and would like to know if it contains a value for a given keyPath:
[myObject valueForKeyPath:myKeyPath];
Now, I wrap it into a #try{ } #catch{} block to avoid exceptions when the given keypath isn't found. Is there a nicer way to do this? Check if the given keypath exists without handling exceptions?
Thanks a lot,
Stefan
You could try this:
if ([myObject respondsToSelector:NSSelectorFromString(myKeyPath)])
{
}
However, that may not correspond to the getter you have, especially if it is a boolean value. If this doesn't work for you, let me know and I'll write you up something using reflection.
For NSManagedObjects, an easy solution is to look at the object's entity description and see if there's an attribute with that key name. If there is, you can also take it to the next step and see what type of an attribute the value is.
Here's a simple method that given any NSManagedObject and any NSString as a key, will always return an NSString:
- (NSString *)valueOfItem:(NSManagedObject *)item asStringForKey:(NSString *)key {
NSEntityDescription *entity = [item entity];
NSDictionary *attributesByName = [entity attributesByName];
NSAttributeDescription *attribute = attributesByName[key];
if (!attribute) {
return #"---No Such Attribute Key---";
}
else if ([attribute attributeType] == NSUndefinedAttributeType) {
return #"---Undefined Attribute Type---";
}
else if ([attribute attributeType] == NSStringAttributeType) {
// return NSStrings as they are
return [item valueForKey:key];
}
else if ([attribute attributeType] < NSDateAttributeType) {
// this will be all of the NSNumber types
// return them as strings
return [[item valueForKey:key] stringValue];
}
// add more "else if" cases as desired for other types
else {
return #"---Unacceptable Attribute Type---";
}
}
If the key is invalid or the value can't be made into a string, the method returns an NSString error message (change those blocks to do whatever you want for those cases).
All of the NSNumber attribute types are returned as their stringValue representations. To handle other attribute types (e.g.: dates), simply add additional "else if" blocks. (see NSAttributeDescription Class Reference for more information).
If the object is a custom class of yours, you could override valueForUndefinedKey: on your object, to define what is returned when a keypath doesn't exist.
It should be possible to graft this behavior onto arbitrary classes reasonably simply. I present with confidence, but without warranty, the following code which you should be able to use to add a non-exception-throwing implementation of valueForUndefinedKey: to any class, with one, centralized line of code per class at app startup time. If you wanted to save even more code, you could make all the classes you wanted to have this behavior inherit from a common subclass of NSManagedObject and then apply this to that common class and all your subclasses would inherit the behavior. More details after, but here's the code:
Header (NSObject+ValueForUndefinedKeyAdding.h):
#interface NSObject (ValueForUndefinedKeyAdding)
+ (void)addCustomValueForUndefinedKeyImplementation: (IMP)handler;
#end
Implementation (NSObject+ValueForUndefinedKeyAdding.m):
#import "NSObject+ValueForUndefinedKeyAdding.h"
#import <objc/runtime.h>
#import <objc/message.h>
#implementation NSObject (ValueForUndefinedKeyAdding)
+ (void)addCustomValueForUndefinedKeyImplementation: (IMP)handler
{
Class clazz = self;
if (clazz == nil)
return;
if (clazz == [NSObject class] || clazz == [NSManagedObject class])
{
NSLog(#"Don't try to do this to %#; Really.", NSStringFromClass(clazz));
return;
}
SEL vfuk = #selector(valueForUndefinedKey:);
#synchronized([NSObject class])
{
Method nsoMethod = class_getInstanceMethod([NSObject class], vfuk);
Method nsmoMethod = class_getInstanceMethod([NSManagedObject class], vfuk);
Method origMethod = class_getInstanceMethod(clazz, vfuk);
if (origMethod != nsoMethod && origMethod != nsmoMethod)
{
NSLog(#"%# already has a custom %# implementation. Replacing that would likely break stuff.",
NSStringFromClass(clazz), NSStringFromSelector(vfuk));
return;
}
if(!class_addMethod(clazz, vfuk, handler, method_getTypeEncoding(nsoMethod)))
{
NSLog(#"Could not add valueForUndefinedKey: method to class: %#", NSStringFromClass(clazz));
}
}
}
#end
Then, in your AppDelegate class (or really anywhere, but it probably makes sense to put it somewhere central, so you know where to find it when you want to add or remove classes from the list) put this code which adds this functionality to classes of your choosing at startup time:
#import "MyAppDelegate.h"
#import "NSObject+ValueForUndefinedKeyAdding.h"
#import "MyOtherClass1.h"
#import "MyOtherClass2.h"
#import "MyOtherClass3.h"
static id ExceptionlessVFUKIMP(id self, SEL cmd, NSString* inKey)
{
NSLog(#"Not throwing an exception for undefined key: %# on instance of %#", inKey, [self class]);
return nil;
}
#implementation MyAppDelegate
+ (void)initialize
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[MyOtherClass1 addCustomValueForUndefinedKeyImplementation: (IMP)ExceptionlessVFUKIMP];
[MyOtherClass2 addCustomValueForUndefinedKeyImplementation: (IMP)ExceptionlessVFUKIMP];
[MyOtherClass3 addCustomValueForUndefinedKeyImplementation: (IMP)ExceptionlessVFUKIMP];
});
}
// ... rest of app delegate class ...
#end
What I'm doing here is adding a custom implementation for valueForUndefinedKey: to the classes MyOtherClass1, 2 & 3. The example implementation I've provided just NSLogs and returns nil, but you can change the implementation to do whatever you want, by changing the code in ExceptionlessVFUKIMP. If you remove the NSLog, and just return nil, I suspect you'll get what you want, based on your question.
This code NEVER swizzles methods, it only adds one if it's not there. I've put in checks to prevent this from being used on classes that already have their own custom implementations of valueForUndefinedKey: because if someone put that method in their class, there's going to be an expectation that it will continue to get called. Also note that there may be AppKit code that EXPECTS the exceptions from the NSObject/NSManagedObject implementations to be thrown. (I don't know that for sure, but it's a possibility to consider.)
A few notes:
NSManagedObject provides a custom implementation for valueForUndefinedKey: Stepping through its assembly in the debugger, all it appears to do is throw roughly the same exception with a slightly different message. Based on that 5 minute debugger investigation, I feel like it ought to be safe to use this with NSManagedObject subclasses, but I'm not 100% sure -- there could be some behavior in there that I didn't catch. Beware.
Also, as it stands, if you use this approach, you don't have a good way to know if valueForKey: is returning nil because the keyPath is valid and the state happened to be nil, or if it's returning nil because the keyPath is invalid and the grafted-on handler returned nil. To do that, you'd need to do something different, and implementation specific. (Perhaps return [NSNull null] or some other sentinel value, or set some flag in thread-local storage that you could check, but at this point is it really all that much easier than #try/#catch?) Just something to be aware of.
This appears to work pretty well for me; Hope it's useful to you.
There's no easy way to solve this. Key Value Coding (KVC) isn't intended to be used that way.
One thing is for sure: using #try-#catch is really bad since you're very likely to leak memory etc. Exceptions in ObjC / iOS are not intended for normal program flow. They're also very expensive (both throwing and setting up the #try-#catch IIRC).
If you look at the Foundation/NSKeyValueCoding.h header, the comment / documentation for
- (id)valueForKey:(NSString *)key;
clearly states which methods need to be implemented for -valueForKey: to work. This may even use direct ivar access. You would have to check each one in the order described there. You need to take the key path, split it up based on . and check each part on each subsequent object. To access ivars, you need to use the ObjC runtime. Look at objc/runtime.h.
All of this is vary hacky, though. What you probably want is for your objects to implement some formal protocol and then check -conformsToProtocol: before calling.
Are your key paths random strings or are those strings under your control? What are you trying to achieve? Are you solving the wrong problem?
I don't believe this is possible in a safe way (i.e. without mucking with -valueForUndefinedKey: or something similar on other peoples' classes). I say that because on the Mac side of things, Cocoa Bindings—which can be set to substitute a default value for invalid key paths—simply catches the exceptions that result from bad key paths. If even Apple's engineers don't have a way to test if a key path is valid without trying it and catching the exception, I have to assume that such a way doesn't exist.