📜 ⬆️ ⬇️

Using the NSProxy class on a simple example

Hello. Today we will talk about the practical use of NSProxy class, the post will be small, I think it will be interesting to read for many beginners.

A little offtopic.

In general, the distribution in the company, I got on Legacy project on Objc, where the massive view controller was considered the benchmark of the architecture. Of course, without normal specs and tests. For a short development career, I have developed a couple of rules for such projects, the first is no refactoring on my part without instructions from above, the second is to touch the existing code only when absolutely necessary. All this is due to the fact that in such projects it is very difficult to catch regression (or maybe I'm just lazy?).

Well, for me, this project is legally noteworthy because in terms of architecture there is no such project, there are 1000 lines of controllers, in short, the project is ideal for experiments - it’s impossible to do worse, and the absence of hard deadlines has only this :)
')
In general, I received a ticket to expand the existing functionality associated with Google maps (Google maps). The project code is rather confused, strongly connected, so I decided not to refactor anything and not to touch it at all for fear of breaking something.

The ticket itself is simple - add a new type of points on the map and perform certain actions on it. It's simple. Let's take a step-by-step look at the evolution of the solution to this task and where the NSProxy class has helped us here

Solution 1 (in the forehead)


Of course, especially for a beginner, the thought comes to mind, just add a couple of methods to an already existing controller, the first to request points via the WebService and the second method to display the obtained points on the map. Those who are not familiar with Google maps, when taped on the map marker, its delegate works

- (BOOL) mapView: (GMSMapView *) mapView didTapMarker: (GMSMarker *) marker {}

And the logic for handling tapas is already added to the existing method by the next if block, since there already existed a large number of different points on the map before the introduction of the new feature.

I decided to give up this method, since the controller is already quite bold, and besides, the new functionality does not overlap with the existing one and is simply boring and serious.

Solution 2


Create a separate controller (not to be confused with the View controller), in which there will be a request for points and their drawing on the map transferred to the Google controller, and to which we will delegate the callback from the tap to the marker from the existing controller. It would look like this (the code for the new functionality in the old controller):

- (void)viewDidLoad { [super viewDidLoad]; self.mapView.delegate = self; self.newLogicController = [[NewLogicController alloc] initWithMapView:self.mapView]; [self.newLogicController load]; } - (BOOL)mapView:(GMSMapView *)mapView didTapMarker:(GMSMarker *)marker { if ([self canHandleMarker:marker]) { // old logic } else { // new logic NewLogicController *controller = self.newLogicController; return [controller mapView:mapView didTapMarker:marker]; } } 

That is, we create a controller and then simply delegate to it tapas on the map for newly added points. It seems simple, it looks good, but I don’t like this if for branching old and new logic (maybe a matter of taste or boredom again?). Although the if-s in the real application was much larger and with complex logic. It was decided to touch the existing code as little as possible; for this, it was necessary to somehow tell Google the map that it might have several delegates. The idea was that the delegate, when calling his method, would determine whether a call was made for him at all, if not for him, then forward the call to the next delegate, if for him, then handle the call and terminate the call chain. Here we smoothly flow to the next solution.

Solution 3


The essence of the idea is to create a class that will support the delegate protocol of Google Maps, but in fact it will not contain its implementation, but will simply delegate calls to real delegates. Moreover, in my case, first delegate the call to the non-core delegate (that is, the one with the new logic), and only then, if he answers that he is not supported, to the main delegate.

The NSProxy class is ideally suited to us for this. Let me remind you that this is an abstract class that does not have a super class (hi NSObject), since it is abstract, then to use it you must inherit from it and implement abstract methods. Actually, the documentation clearly tells us which methods we must implement, these are a couple of methods:

 - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel; - (void)forwardInvocation:(NSInvocation *)invocation; 

These methods will be called when your heir proxy class sent any message, and will be called in the same order in which they are written. In these methods, your proxy should forward calls to other real objects. In fact, there is a shorter way of proxying through the method:

 - (id)forwardingTargetForSelector:(SEL)aSelector; 

if the object for which the call is intended is returned from it, then the methods:

 - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel; - (void)forwardInvocation:(NSInvocation *)invocation; 

will not be called, if you return nil, then the turn of these methods comes.

The final simplified view of the proxy class will look something like this:

 @interface GoogleMapViewProxy : NSProxy <GMSMapViewDelegate> @property (nonatomic, weak) id fakeDelegate; @property (nonatomic, weak) id originDelegate; - (instancetype)init; @end @implementation GoogleMapViewProxy - (instancetype)init { return self; } - (id)forwardingTargetForSelector:(SEL)aSelector { if ([self.fakeDelegate respondsToSelector:aSelector]) { return self.fakeDelegate; } else if ([self.originDelegate respondsToSelector:aSelector]) { return self.originDelegate; } else { return nil; } } - (BOOL)respondsToSelector:(SEL)aSelector { if ([self.fakeDelegate respondsToSelector:aSelector]) { return YES; } else if ([self.originDelegate respondsToSelector:aSelector]) { return YES; } else { return NO; } } - (void)forwardInvocation:(NSInvocation *)invocation { [invocation invokeWithTarget:self.originDelegate]; } - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel { return [self.originDelegate methodSignatureForSelector:sel]; } @end 

Everything important happens in the method:

 - (id)forwardingTargetForSelector:(SEL)aSelector; 

although we can easily transfer this code to:

 - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel; 

just then the call chain will be a little longer about what I wrote above.

As we can see, if a new delegate responds to a message, then the call is delegated to it, if not, then we just call the original (this proxy does not take into account the moment when our new delegate implements methods that the original delegate does not implement, in this implementation we will drop into DoesNotRecognizeSelector , for my case, this situation does not happen, so I dropped this moment).

The final version of the code in the controller with the old logic remains almost unchanged:

 - (void)viewDidLoad { [super viewDidLoad]; self.mapView.delegate = self; self.newLogicController = [[NewLogicController alloc] initWithMapView:self.mapView]; [self.newLogicController load]; } 

and the code in the controller with the new logic:

 @interface NewLogicController () <GMSMapViewDelegate> @property (nonatomic) NSMutableArray<GMSMarker *> *markers; @property (nonatomic) GoogleMapViewProxy *mapProxy; @end @implementation NewLogicController - (instancetype)initWithMapView:(GMSMapView *)mapView { self = [super init]; if (self) { _markers = [NSMutableArray array]; _mapProxy = [[GoogleMapViewProxy alloc] init]; _mapProxy.fakeDelegate = self; _mapProxy.originDelegate = mapView.delegate; mapView.delegate = _mapProxy;//   ,        } return self; } - (BOOL)mapView:(GMSMapView *)mapView didTapMarker:(GMSMarker *)marker { if ([self.markers containsObject:marker]) { //   return NO; } //           return [self.mapProxy.originDelegate mapView:mapView didTapMarker:marker]; } - (void)load { // ... } @end 

Results


The implementation of this task through a proxy allowed us to add functionality to the existing code with minimal changes for it. If in the future we need to abandon this functionality, then for this we only need to delete the creation of the controller. High reuseability - to add this feature to another screen, we just need to create a controller and give it a google map.

The post turned out to be small, so I will share another interesting case where NSProxy can be used - to refuse tedious type checks:

 id<SomeDelegate> delegate = self.delegate; if ([delegate respondsToSelector:@selector(someMethod:)]) { [delegate someMethod:self]; } 

that is, immediately write without fear of falling in the DoesNotRecognizeSelector:

 [self.delegate someMethod:self]; 

I will not repeat how to achieve this, everything is very detailed in the article by Peter Steinberger.

All cookies and thank you for your attention.

Update.


The method of using the NSProxy class considered in the article would be appropriate rather only in a fragile project. Therefore, in projects under active development, it is not worthwhile to adopt this trick.

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


All Articles