📜 ⬆️ ⬇️

NSProxy, as a way to cut corners

As many have read in books, there are initially two root classes in the Objective-C language - NSObject and NSProxy. And if almost everything is based on the first one and it is impossible not to face it, then the second one is used much less often. In this small article I will describe the applications of this class that I had to use.

As a first example, let me remind you about such a thing as UIAppearance — as far as I know — the only use of NSProxy in basic iOS frameworks. Its task is to pre-configure the group of UIKit objects in one place. In fact, you describe some set of actions that will be applied to each created object (such as specifying colors, fonts, and others) that satisfy certain conditions (there are currently two conditions: object class and object class containing our object as a subview). There is a wonderful article on the use, capabilities, and side effects of such a tool, so we will no longer dwell on this.

To be honest, it is not thick. For such a proud status as “one of two root classes,” there are too few examples of constructive use. According to my personal experience and the experience of my familiar developers, this makes the initial understanding of this entity rather difficult and, as a result, it dissuades us from using it. But it is interesting! And over time, tasks began to appear, for which the NSProxy is a tremendous convenience tool.

Decorating objects


In the implementation of the “decorator” pattern there is one rather inconvenient aspect - the interface of the decorator itself. If we have some hierarchy of objects, for example
')
@interface SomeClass : NSObject <SomeClassInterface> @end @interface ChildClass : SomeClass -(void) additionalMethod; @end @interface DecoratorClass : NSObject <SomeClassInterface> ... ChildClass *instance = [ChildClass new]; id decoratedInstance = [DecoratorClass decoratedInstanceOf:instance] 

That for obvious reasons, decoratedInstance will no longer be able to perform additionalMethod. And it remains for us to either write categories, or introduce some hooks into the root class, or deal with some other similar indecency. Now let's see how this can be solved using NSProxy.
 @interface SomeClass : NSObject; -(int) getNumber; -(NSString*) getString; @end @interface SimpleDecorator : NSProxy @property (nonatomic, strong) SomeClass *instance; +(instancetype) decoratedInstanceOf:(SomeClass*)instance; @end @implementation SimpleDecorator -(instancetype) initWithObject:(SomeClass*)object { /*   - NSProxy    ,  NSObject.   [super init]  */ _instance = object; return self; } +(instancetype) decoratedInstanceOf:(SomeClass*)instance { return [[self alloc] initWithObject:instance]; } /*   NSProxy -    ,      ,      .  ,    ,     -    SomeClass    */ - (NSMethodSignature *)methodSignatureForSelector:(SEL)selector { return [self.instance methodSignatureForSelector:selector]; } - (void)forwardInvocation:(NSInvocation *)invocation { /*   , , ,       - ,    ,        , ,    getString */ [invocation invokeWithTarget:self.instance]; } /*   .         NSProxy -       */ - (int) getNumber { return [self.instance getNumber] + 1; } @end 

And, actually, an example of using this design:
  SomeClass *object = [SomeClass new]; object = (SomeClass*)[SimpleDecorator decoratedInstanceOf:object]; NSLog(@"%d", [object getNumber]); NSLog(@"%@", [object getString]); 

With this approach, on site SomeClass can be any of its heirs and the resulting decorated object will correctly respond to all messages sent to it.

Delayed setting


In fact, we will now solve the problem, similar to the one that UIAppearance solves. When working on a project built on the basis of the architecture proposed by the PureMVC pattern library ( en.wikipedia.org/wiki/PureMVC ), we had to repeatedly encounter a situation where, at some point in the code, we initiate a chain of command-actions, which will result in some context-sensitive entity — for example, a pop-up window, closing of which triggers the following actions, depending on the context in which the window was called. Previously, one of two options was used for this:
- Broach. Through the entire chain of actions, we transmit additional, somehow structured data that we process on the spot where the window was created. A rather unprofitable and not very beautiful way, especially if some actions involve branching.
- "I know everything". Since PureMVC makes the main objects, in fact, singletons, and you can reach anyone at any time - you can try to get all the necessary context in the pop-up window command, which is fraught with a significant increase in code connectivity.

Using NSProxy, you can achieve a slightly different behavior: Set context-dependent properties to an object BEFORE it was created - in the place where we know these conditions. Sounds, of course, somewhat absurd, but somehow it works. We treat the existing NSProxy object as a target object that is not yet available. NSProxy keeps inside of itself the actions that we have taken and when an object is instantiated - it applies them to it.
Now for some code.

First, the class we want to use for deferred configuration:
 /*   ,   . */ @interface Popup : NSObject; /*    ,    ,      */ @property (nonatomic, strong) NSString* key; @property (nonatomic, strong) NSString* message; /*       ,        ,     UIAppearance */ +(DelayedActionsProxy*) delayedInitializerForKey:(NSString*)key; /*   .      ,      */ -(void) showPopup @end @implementation Popup -(void) showPopup { [DelayedActionsProxy invokeDelayedInvocationsWithTarget:self]; /*...*/ } +(DelayedActionsProxy*) delayedInitializerForKey:(NSString*)key { return [DelayedActionsProxy sharedProxyForKey:key fromClass:[self class]]; } @end 

And now, actually, proxy
 @interface DelayedActionsProxy : NSProxy +(void) invokeDelayedInvocationsWithTarget:(Popup*) target; +(instancetype) sharedProxyForKey:(NSString*)key fromClass:(Class)objectClass; @end @interface DelayedActionsProxy() /*             ,    -      */ @property (nonatomic, strong) NSString *currentKey; @property (nonatomic, assign) Class currentClass; @property (nonatomic, strong) NSMutableDictionary *delayedInvocations; @end @implementation DelayedActionsProxy -(instancetype) init { self.delayedInvocations = [NSMutableDictionary new]; return self; } static DelayedActionsProxy *proxy = nil; +(instancetype) sharedProxyForKey:(NSString*)key fromClass:(Class)objectClass { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ proxy = [[self alloc] init]; }); proxy.currentKey = key; proxy.currentClass = objectClass; return proxy; } /*      [[Popup delayedInitializerForKey:@"key"] setText:@"someText"],      -     ,     currentKey  currentClass */ - (NSMethodSignature *)methodSignatureForSelector:(SEL)selector { /*   currentClass   ,   ,     instanceMethodSignature  methodSignature */ return [self.currentClass instanceMethodSignatureForSelector:selector]; } - (void)forwardInvocation:(NSInvocation *)invocation { if (!self.delayedInvocations[self.currentKey]) { self.delayedInvocations[self.currentKey] = [NSMutableArray new]; } /*     ,     */ [self.delayedInvocations[self.currentKey] addObject:invocation]; } /*      */ +(void) invokeDelayedInvocationsWithTarget:(Popup*) target { for (NSInvocation *invocation in proxy.delayedInvocations[proxy.currentKey]) { [invocation invokeWithTarget:target]; } [proxy.delayedInvocations removeObjectForKey:proxy.currentKey]; } @end 

And an example of using
 [[Popup delayedInitializerForKey:@"key"] setText:@"someText"]; 


Facilitate working with UI objects


In multithreaded applications, quite often, due to carelessness and insufficient planning at the initial stage, you can get code that runs in a different thread, but with which passion is how to modify the UI (or database, or something else that is very sensitive to multithreaded access). The code starts to grow into numerous performSelectorOnMainThread, dispatch_async, or worse - wrappers over NSInvocation, because performSelectorOnMainThread does not allow using more than one parameter. Why not get a single wrapper for this?

Suppose we have some object (for example, an object in the game on Cocos2D)
 @interface Entity : NSObject; @property (nonatomic, strong) CCNode* node; @end @implementation Entity -(void)setRepresentation:(CCNode *)node { /* -     ... */ _node = (CCNode*)[MainThreadProxy node]; } @end 

And the proxy itself
 @interface MainThreadProxy : NSProxy +(instancetype) proxyWithObject:(id)object; /*        ,            ,    */ -(void)performBlock:(void (^)(id object))block; @end @interface MainThreadProxy() @property (nonatomic, strong) id object; @end @implementation MainThreadProxy -(instancetype) initWithObject:(id)object { self.object = object; return self; } +(instancetype) proxyWithObject:(id)object { return [[self alloc] initWithObject:object]; } - (NSMethodSignature *)methodSignatureForSelector:(SEL)selector { return [self.object methodSignatureForSelector:selector]; } - (void)forwardInvocation:(NSInvocation *)invocation { if ([NSThread isMainThread]) { [invocation invokeWithTarget:self.object]; } else { [invocation performSelectorOnMainThread:@selector(invokeWithTarget:) withObject:self.object waitUntilDone:YES]; } } -(void)performBlock:(void (^)(id object))block { if ([NSThread isMainThread]) { block(self.object); } else { dispatch_sync(dispatch_get_main_queue(), ^{block(self.object);}); } } @end 

An example of use is quite common.
 [entity.node render]; 

But we have a guarantee that this can be caused perfectly correctly from any thread.

Completion


As ideas for what you can use a proxy, you can still highlight such things as
- Masking a remote object - to work with an object, which is the representation of a service with non-fixed response time (for example, a database on a site, an object on a client that you are connected to via BlueTooth), like a normal object. Just slow.
- Wrapper in Proxy Timer, so that syntax like is appropriate
 [[object makeCallWithTimeInterval:1.0f andRepeatCount:2] someMethod]; 

other.
It should be borne in mind that this is, of course, not a panacea. You should be very careful when working with getters, for example, in the example above, the line
 [entity.node.effect update]; 

will not behave correctly. This, of course, can be fixed, but these fixes also have their price. I recommend not to forget about it when working with NSProxy.

Post Scriptum


In general, you can read about NSProxy even for example here , or here . NSProxy is deeply used in a tool like OCMock. Anyway, this is a fairly flexible and convenient tool, suitable for a certain class of tasks - and, at least, it has made sense to become familiar with it.

Source: https://habr.com/ru/post/235041/


All Articles