NSProxy, NSObject’s Lesser-Known Sibling

When we were testing URLMock, one of the first things we needed to verify was that we were sending the appropriate messages to NSURLConnection delegates. URLMock can send response data in multiple chunks, so we needed to check that certain delegate messages were received multiple times, while others were received once or not at all.

One way to do this would be to add a counter property to our NSURLConnectionDelegate object for each message in the delegate protocol. Then, in our delegate method implementations, we would just increment the appropriate counter. During unit testing, we would just have to verify that each counter has the expected value.

This whole plan of attack is kind of gross. It’s very specific to the one delegate we’re testing, so if we ever need to adapt the pattern to a different protocol, we can’t reuse much of our code. It also doesn’t scale well when you have a lot of delegate methods. Still, I liked the basic idea of counting messages and using those counters in our tests. To implement the idea more elegantly, I decided to use Objective-C message forwarding.

But before we get to that…

A Brief Digression on Sending Messages

Any introduction to Objective‑C worth its salt mentions that unlike Java and C#, you don’t call methods in Objective-C, you send messages to objects. The distinction is subtle, so most people just nod their heads and move on. However, the message-oriented nature of Objective‑C is one of its key differentiators from other compiled object-oriented languages. For example, message sending allows you to send the same message to objects of completely different types:

for (id collection in @[ mutableArray, mutableSet, mutableOrderedSet ]) {
    [collection addObject:@"Hooray for unbounded polymorphism!"];
}

You can also construct and send messages dynamically at runtime:

- (NSSet *)validatorsForKey:(NSString *)key
{
    NSString *capitalizedKey = [key twt_capitalizedCamelCaseString];
    NSString *selectorString = [NSString stringWithFormat:@"validatorsFor%@", capitalizedKey];
    SEL selector = NSSelectorFromString(selectorString);
    return [self respondsToSelector:selector] ? [self performSelector:selector] : nil;
}

If you’re feeling feisty, you can do less typical things, like disavowing part of your superclass’s interface:

- (instancetype)init
{
    // Don’t respond to -init. –initWithObject: should always be used
    [self doesNotRecognizeSelector:_cmd];
    return nil;
}

Finally, you can forward messages to another object entirely. It’s this concept that we can use to easily and generally solve the problem of counting how many times an object has received a given message.

Forwarding Messages with NSProxy

To implement our message counter, we’re going to subclass one of Cocoa’s more esoteric classes, NSProxy. As the name implies, NSProxy objects stand in for one or more other objects. They are used almost exclusively to forward messages.

NSProxy is unique in that it’s the only other root class besides NSObject in the Cocoa frameworks. That is, like NSObject, it has no superclass. The two root classes do have partially overlapping interfaces via the NSObject protocol, which declares messages like +alloc, ‑respondsToSelector:, and ‑performSelector:, but for the most part NSProxy responds to far fewer messages.

To forward messages with NSProxy, you need to override two methods: ‑methodSignatureForSelector: and ‑forwardInvocation:.

  • ‑methodSignatureForSelector: responds with an NSMethodSignature object that describes the arguments and return type of the given selector. Returning nil implies that the proxy does not recognize the specified selector. All NSObjects respond to this message as well.
  • -forwardInvocation: does the actual business of forwarding a particular message to the appropriate object.

Now that we’ve got a grip on NSProxy, let’s implement our message counter.

Message Counting Proxy

The idea of our message counting proxy is pretty simple. We’ll create an NSProxy subclass called UMKMessageCountingProxy which has two fundamental properties: the object for which it’s counting received messages—its proxied object—and a mutable dictionary that maps selectors to the number of times they’ve been received. To use a message counting proxy, we’ll just do something like this:

id messageCountingProxy = [[UMKMessageCountingProxy alloc] initWithObject:realObject];
[messageCountingProxy foo];

…

NSUInteger count = [messageCountingProxy receivedMessageCountForSelector:@selector(foo:)];

Okay, so let’s get to work. UMKMessageCountingProxy’s interface looks like:

@interface UMKMessageCountingProxy : NSProxy

@property (nonatomic, strong, readonly) NSObject *object;

- (instancetype)initWithObject:(NSObject *)object;
- (NSUInteger)receivedMessageCountForSelector:(SEL)selector;

@end

Our implementation file starts with a private class extension and our initializer.

@interface UMKMessageCountingProxy ()
@property (nonatomic, strong, readonly) NSMutableDictionary *receivedMessageCounts;
@end


@implementation UMKMessageCountingProxy

- (instancetype)initWithObject:(NSObject *)object
{
    NSParameterAssert(object);

    // Don't call [super init], as NSProxy does not recognize -init.
    _object = object;
    _receivedMessageCounts = [[NSMutableDictionary alloc] init];

    return self;
}

There’s nothing unusual here except that our initializer doesn’t call its superclass’s. This is because there isn’t one; NSProxy does not respond to ‑init.

Next, let’s implement message forwarding. We’ll start by overriding ‑methodSignatureForSelector:. We’re supposed to return an NSMethodSignature object for the specified selector. While that might seem complicated, it’s actually really easy. Since we’ll be forwarding the message to the our proxied object, we just ask it for the appropriate method signature.

- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector
{
    return [self.object methodSignatureForSelector:selector];
}

Done.

Next, we need to implement -forwardInvocation:. Our goal here is to increment the count for the appropriate selector, and then re-route the invocation to our proxied object. To do this, we’ll convert the invocation’s selector to a string and use that to look up and increment the current received message count in our receivedMessageCounts dictionary. We’ll then use ‑[NSInvocation invokeWithTarget:] to re-invoke the invocation with a different target, namely our proxied object.

- (void)forwardInvocation:(NSInvocation *)invocation
{
    NSString *selectorString = NSStringFromSelector(invocation.selector);
    NSUInteger count = [self.receivedMessageCounts[selectorString] unsignedIntegerValue];
    self.receivedMessageCounts[selectorString] = @(count + 1);
    [invocation invokeWithTarget:self.object];
}

And we’re done with message forwarding. All that’s left is to implement ‑receivedMessageCountForSelector: to return the number of times we’ve responded to a given selector. This is implemented exactly how you’d expect:

- (NSUInteger)receivedMessageCountForSelector:(SEL)selector
{
    return [self.receivedMessageCounts[NSStringFromSelector(selector)] unsignedIntegerValue];
}

And that’s all there is to it. Take a look at the full interface and implementation files to see it all put together.

Summary

This is just one use of NSProxy, but there are plenty of others: OCMock uses NSProxy to implement mock objects. You could also use NSProxy to implement the Decorator pattern in Objective-C. On OS X (but not iOS), Apple provides NSProtocolChecker, a class that only forwards messages to an object if the messages appear in a particular protocol. Creating an iOS implementation is a moderately challenging exercise that I recommend you try. Here’s my solution.

In any case, it’s unlikely that you’ll frequently subclass NSProxy. Still, it’s a powerful class that illustrates how Objective-C’s message-oriented nature can be harnessed to solve certain problems elegantly and concisely, and it’s great to have in your bag of tricks.

Have questions or other potential uses of NSProxy? Get in touch with me on Twitter at @prachigauriar.

Tweet about this on Twitter