Search code examples
iosobjective-cfacebook-sdk-3.0

NSDictionary<FBGraphUser> *user syntax explanation


In the Facebook iOS SDK requests are returned with the following handler:

 ^(FBRequestConnection *connection, 
       NSDictionary<FBGraphUser> *user, 
       NSError *error) { }

The user variable can then be accessed with calls like these...

   self.userNameLabel.text = user.name;
   self.userProfileImage.profileID = user.id;

This syntax is somewhat similar to the syntax id <protocolDelegate> object syntax that is a common property declaration, except for that the NSDictionary is the id object explicitely, and that dictionary conforms to the protocol? But where does the dot syntax come from and how does one state that an arbitrary NSFoundation object corresponds to a protocol without subclassing the object itself and making it conform?

I did some additional research about dot notation and NSDictionary and it appears that it is not possible to use dot notation on a dictionary without adding a category to NSDictionary. However, I did not see any reference of the <> syntax in the Apple Documentation to indicate that this particular instance of NSDictionary conformed to that notation.

And the Facebook documentation is a little sparse on how this wrapping works:

The FBGraphUser protocol represents the most commonly used properties of a Facebook user object. It may be used to access an NSDictionary object that has been wrapped with an FBGraphObject facade.

If one follows this lead to the FBGraphObject documentation then there is methods that return dictionaries that conform to this "facade..." but no further explanation on how one goes about wrapping a dictionary.

So I guess my questions are a few:

  1. What would the underlying code look like to make this sort of syntax work?
  2. Why does it exist?
  3. Why would facebook implement it this way as opposed to just making an object that they can convert the data into?

Any explanation or insight would be very appreciated!


Solution

  • Basically, NSDictionary<FBGraphUser> *user, implies an object that inherits from NSDictionary, adding functionality (specifically, typed access) declared by the FBGraphUser protocol.

    The reasons behind this approach are described in quite a bit of detail in the FBGraphObject documentation (the FBGraphUser protocol extends the FBGraphObject protocol). What might be confusing you is that FBGraphObject is a protocol (described here) and a class (described here), which inherits from NSMutableDictionary.

    In terms of inner implementation, it's some pretty advanced Objective-C dynamic magic, which you probably don't want to worry about. All you need to know is you can treat the object as a dictionary if you wish, or use the additional methods in the protocol. If you really want to know the details, you can look at the source code for FBGraphObject, in particular, these methods:

    #pragma mark -
    #pragma mark NSObject overrides
    
    // make the respondsToSelector method do the right thing for the selectors we handle
    - (BOOL)respondsToSelector:(SEL)sel
    {
        return  [super respondsToSelector:sel] ||
        ([FBGraphObject inferredImplTypeForSelector:sel] != SelectorInferredImplTypeNone);
    }
    
    - (BOOL)conformsToProtocol:(Protocol *)protocol {
        return  [super conformsToProtocol:protocol] ||
        ([FBGraphObject isProtocolImplementationInferable:protocol 
                               checkFBGraphObjectAdoption:YES]);
    }
    
    // returns the signature for the method that we will actually invoke
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
        SEL alternateSelector = sel;
    
        // if we should forward, to where?
        switch ([FBGraphObject inferredImplTypeForSelector:sel]) {
            case SelectorInferredImplTypeGet:
                alternateSelector = @selector(objectForKey:);
                break;
            case SelectorInferredImplTypeSet:
                alternateSelector = @selector(setObject:forKey:);
                break;
            case SelectorInferredImplTypeNone:
            default:
                break;
        }
    
        return [super methodSignatureForSelector:alternateSelector];
    }
    
    // forwards otherwise missing selectors that match the FBGraphObject convention
    - (void)forwardInvocation:(NSInvocation *)invocation {
        // if we should forward, to where?
        switch ([FBGraphObject inferredImplTypeForSelector:[invocation selector]]) {
            case SelectorInferredImplTypeGet: {
                // property getter impl uses the selector name as an argument...
                NSString *propertyName = NSStringFromSelector([invocation selector]);
                [invocation setArgument:&propertyName atIndex:2];
                //... to the replacement method objectForKey:
                invocation.selector = @selector(objectForKey:);
                [invocation invokeWithTarget:self];
                break;
            }
            case SelectorInferredImplTypeSet: {
                // property setter impl uses the selector name as an argument...
                NSMutableString *propertyName = [NSMutableString stringWithString:NSStringFromSelector([invocation selector])];
                // remove 'set' and trailing ':', and lowercase the new first character
                [propertyName deleteCharactersInRange:NSMakeRange(0, 3)];                       // "set"
                [propertyName deleteCharactersInRange:NSMakeRange(propertyName.length - 1, 1)]; // ":"
    
                NSString *firstChar = [[propertyName substringWithRange:NSMakeRange(0,1)] lowercaseString];
                [propertyName replaceCharactersInRange:NSMakeRange(0, 1) withString:firstChar];
                // the object argument is already in the right place (2), but we need to set the key argument
                [invocation setArgument:&propertyName atIndex:3];
                // and replace the missing method with setObject:forKey:
                invocation.selector = @selector(setObject:forKey:);
                [invocation invokeWithTarget:self]; 
                break;
            } 
            case SelectorInferredImplTypeNone:
            default: 
                [super forwardInvocation:invocation];
                return;
        }
    }