1

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?

jrtc27
  • 8,496
  • 3
  • 36
  • 68
  • 1
    Did you mean to say "as *not* all devices which support Game Center can update to iOS 5?" Game Center was added in iOS 4.1. My 2nd generation iPod touch is stuck on iOS 4.2.1, so has Game Center but will never run iOS 5.0. – Cowirrie Aug 29 '12 at 05:40
  • Oh right, I see, I assumed that the 2nd Generation iPod Touch would be the same as the iPhone 3G, but it's not. Thanks for that information. – jrtc27 Aug 29 '12 at 09:14

4 Answers4

4

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.

Jody Hagins
  • 27,943
  • 6
  • 58
  • 87
  • Yep, I can confirm this behaviour. Furthermore, in iOS 6, the `instancesRespondToSelector:` implementation has been overridden, explaining why it works as expected. Seeing as you have got to the root of the problem, I shall award you the bounty, but the correct answer shall remain as it is, being the first answer, which also gave a work-around which I somehow managed to miss. – jrtc27 Sep 04 '12 at 12:29
1

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
Cowirrie
  • 7,218
  • 1
  • 29
  • 42
  • Also, for the record, iOS 6 acts as expected. And I hardly think that needs to be protected by an NDA :P I won't award the bounty yet in case someone is able to explain why. – jrtc27 Aug 29 '12 at 09:45
  • No problem. I'd really like to know the reason for this odd behavior myself. – Cowirrie Aug 29 '12 at 09:50
  • Would people frown upon me if I did a quick and dirty test for the "iPod2,1" model name/number to identify an iPod Touch 2G (and therefore give them a different message to all other devices on iOS 4.1+)? EDIT: Never mind, just made the message more generic. – jrtc27 Aug 29 '12 at 09:58
  • Do you mean that on the iPod Touch you'll say "You can't use multiplayer, ever" and on everything else you'll say "Upgrade to iOS 5 for multiplayer"? I think that's reasonable, and don't believe there's any neat way to check a device's maximum supported version. The scary thing is when people enable or disable functionality based on device names - say if you relied on the model name to tell you if the `context` property is available. – Cowirrie Aug 29 '12 at 10:04
  • Oh God no I would never do the latter! I think I'll leave it as a generic "Please update to iOS 5 to play online" message, as that way iPod Touch 2G owners know why they can't play online despite having Game Center. Unless you have a better idea? – jrtc27 Aug 29 '12 at 10:06
  • Anyone who still owns an iPod Touch 2G is either a cheapskate or poor. Either way, we don't buy many apps. So don't worry much about supporting us. According to Apple, 80% of iOS devices were running iOS 5 by June 2012, and that will keep going up. If you end up with tech support requests from older devices, it may be better to just raise your iOS Deployment Target to 5.0. – Cowirrie Aug 29 '12 at 10:13
  • 1
    Yeah, we'll be doing that eventually, but we have some features we'd like to add for all devices first. Sadly, Xcode 4.5 no longer has support for building for armv6 devices, so it looks like that may happen very soon, unless Apple keeps accepting apps built with older versions. – jrtc27 Aug 29 '12 at 10:16
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.

Community
  • 1
  • 1
Ephemera
  • 8,672
  • 8
  • 44
  • 84
  • `-respondsToSelector:` does return the expected result of `YES`, but as I mentioned in a comment on the answer above both work as expected in the iOS 6 betas. I had considered using the Objective-C runtime, but that always seemed much less elegant, and, while I know this isn't true, it *seems* prone to breaking. Anyway thanks for the answer, but I think I'll stick to just checking an allocated instance, as it seems to work. – jrtc27 Aug 31 '12 at 06:47
  • @jrtc27 All good :) just thought I'd let you know about it. As an aside, -respondsToSelector: is itself just a wrapper for the runtime function class_respondsToSelector(Class, SEL), as are most of NSObject's basic methods. You can see NSObject's implementation here: http://www.opensource.apple.com/source/objc4/objc4-532/runtime/NSObject.mm – Ephemera Aug 31 '12 at 09:20
  • Ah that's a good link - thanks for that! I suppose you're right about `class_respondsToSelector`, but the source code makes the behaviour outlined in my question much stranger than I originally thought... Oh well, I *suppose* I could go and dive into some assembly debugging, but my current solution works, so I'm happy :) – jrtc27 Aug 31 '12 at 16:46
1

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.

bowerandy
  • 43
  • 1
  • 6