Objective C. Practice. Events

    The event-oriented logic in Objective-C is based on three pillars - protocols, a notification center, and key-value observing. Traditionally, protocols are used to extend the functionality of base classes without inheritance, key-value observing for interaction between the visual and logical part of an application, and notification center for processing user events.

    Naturally, all this beauty can be safely used to build complex applications. Of course, there is no real need to invent your own bicycles. However, it seemed very unpleasant to me, as the person who came to the development of Objective-C applications from the .NET world, that the notification center, which I planned to use for events, breaks the application stack by writing the event to the queue in the UI thread, and the protocols in the classic view not too convenient, therefore, for convenience, I decided to build myself a mechanism that would be much more similar to what we are used to doing in the .NET world. Thus was born the idea of ​​implementing a model of multiple signatories through a special class called AWHandlersList.

    This article is intended for programmers who have some experience in creating applications on Objective-C and have already written similar bikes, or have solved similar problems in standard ways. This option is not a silver bullet, but has shown itself to be a convenient mechanism that minimizes the writing of code for processing sets of events with different interfaces and parameters.

    The idea of ​​the class is quite simple - it contains a list of signatories, each element of which consists of two components - target and selector.

    What is this bike made for? It seemed to me that it is more convenient than all the standard models presented for some logic related to the transmission of events. Maybe he will help someone make life easier.

    In .NET, the familiar model of event logic offers a delegate with two parameters - sender of type object and args of type inherited from EventArgs. In order not to break our brains, we will do the same. First, let's define an empty EventArgs class from which all event arguments will be inherited.

    @interface AWEventArgs : NSObject
    @end


    Now we define a class that will contain a pair of “target object and method to be called”, adding some debugging information there so that in the future it would be easy to debug the logic of events.

    @interface AWEventHandler : NSObject
    {
    	@private
    	NSString *_description;
    }
    @property (nonatomic, assign) id target;
    @property (nonatomic, assign) SEL method;
    +(AWEventHandler *)handlerWithTarget:(id)target method:(SEL)method;
    @end
    @implementation AWEventHandler
    @synthesize method, target;
    -(id)initWithTarget:(id)t method:(SEL)m;
    {
    	self = [super init];
    	if(self)
    	{
    		target = t;
    		method = m;
    		_description = [[NSString alloc] initWithFormat:@"EventHandler, Target=%@, Method=%@", NSStringFromClass([target class]), NSStringFromSelector(method)];
    	}
    	return self;
    }
    -(NSString *)description
    {
    	return _description;
    }
    -(void)dealloc
    {
    	[_description release];
    	[super dealloc];
    }
    +(AWEventHandler *)handlerWithTarget:(id)target method:(SEL)method
    {
    	AWEventHandler *handler = [[[AWEventHandler alloc] initWithTarget:target method:method] autorelease];
    	return handler;
    }
    @end


    As you can see, both target and method are essentially weak links. This is quite natural - weak references are widely used in the Objective-C world in order to avoid circular references and to allow objects to be automatically freed. Unfortunately, this leads to the fact that, with careless coding, “dead” pointers to objects that drop the application appear everywhere, so I’ll show a little further a beautiful mechanism that can prevent and eliminate their appearance.

    Now, finally, let's move on to our main class - the list of signatories. There are non-trivial moments in the code, but they are solved by reading the documentation, and if there is no desire to understand the issue, then you can simply use it, the code is fully working and removed from the “combat” project.

    @interface AWEventHandlersList : NSObject
    {
    	NSMutableArray *_handlers;
    }
    @property (nonatomic, copy) NSString *name;
    -(void)addReceiver:(id)receiver delegate:(SEL)delegate;
    -(void)removeReceiver:(id)receiver delegate:(SEL)delegate;
    -(void)clearReceivers;
    -(void)invoke;
    -(void)invokeWithSender:(id)sender;
    -(void)invokeWithSender:(id)sender args:(AWEventArgs *)event;
    @property (nonatomic, retain) NSRunLoop *runLoop;
    @end


    I will briefly explain why the fields of this class are needed.

    The first is name. I prefer to name events so that you can see in the logs exactly what event was triggered. Usually, as the name of the event, I use the name of the class, along with the name of the method called in it, to raise the method. This is a convenient practice, since it allows you not to frantically scour the stack in search of whoever threw the event, but simply look at this value in the debug console.

    Methods addReceiverand removeRecevierlogical - they take an object and a selector, which will continue to receive calls.

    Methodsinvokemust throw an event, passing it to signed objects for processing. They are given in three versions - in order not to transmit empty nil values ​​in the event that there is no need for some parameters of the event.

    The method is clearReceiversinternal, it is better to define it in the anonymous section, since the calling code should not be able to unsubscribe other objects from events, but historically it has been put into the interface. This is easy to fix if it seems wrong to you.

    Finally, the propertyrunLoopnecessary if you intend to make sure that some events are tied to a specific thread. For example, this is necessary if there is some code in the worker thread that should update the visual part of the application, or vice versa - from the UI thread there must be access to some worker thread synchronized through the message queue, that is, if there is a need to throw events and process them in different threads.

    Implementing a class is ideally trivial, but requires some understanding of how selectors work. I will clarify the difficult points in the comments in the code itself.

    @implementation AWEventHandlersList
    @synthesize runLoop = _runLoop;
    @synthesize name = _name;
    -(id)init
    {
    	self = [super init];
    	if(!self)
    		return nil;
    	_handlers = [[NSMutableArray alloc] init];
    	return self;
    }
    -(void)addReceiver:(id)receiver delegate:(SEL)delegate
    {
    	/* Этот код можно убрать, если вы гарантируете, что каждый объект будет подписываться на событие только один раз, либо
    	 * вам необходимо множественное подписание. Я предпочитаю работать со страховкой */
    	[self removeReceiver:receiver delegate:delegate]; 
    	[_handlers addObject:[AWEventHandler handlerWithTarget:receiver method:delegate]];
    }
    -(void)removeReceiver:(id)receiver delegate:(SEL)delegate
    {
    	/* В идеале снятие копии со списка, сделанное для поддержки многопоточности, должно производитсья в критической секции
    	 * (NSLock), однако я опустил этот момент, как как у меня подписание на события всегда происходит в одном потоке, 
    	 * а копия списка берется для того, чтобы в будущем достаточно было обернуть вызов в NSLock */
    	for(AWEventHandler *handler in [[_handlers copy] autorelease])
    		if(handler.method == delegate && handler.target == receiver)
    			[_handlers removeObject:handler];
    }
    -(void)clearReceivers
    {
    	[_handlers removeAllObjects];
    }
    -(void)invoke
    {
    	[self invokeWithSender:nil args:nil];
    }
    -(void)invokeWithSender:(id)sender
    {
    	[self invokeWithSender:sender args:nil];	
    }
    -(void)invokeWithSender:(id)sender args:(AWEventArgs *)event
    {
    	[self invokeWithSender:sender args:event runLoop:_runLoop];
    }
    -(void)invokeWithSender:(id)sender args:(AWEventArgs *)event runLoop:(NSRunLoop *)runLoop
    {
    	/* Вс случае, если к текущему потоку не привязан цикл выборки сообщений, метод вернет null и выполнеие будет
    	 * происходить по обычному сценарию */
    	if(!runLoop)
    		runLoop = [NSRunLoop currentRunLoop];
    	NSUInteger order = 1;
    	NSArray *handlersCopy = [NSArray arrayWithArray:_handlers];
    	for(AWEventHandler *handler in handlersCopy)
    		if(runLoop == [NSRunLoop currentRunLoop])
    			[self internalInvoke:[NSArray arrayWithObjects:handler, sender == nil ? [NSNull null] : sender, event == nil ? [NSNull null] : event, nil]];
    		else
    			[runLoop performSelector:@selector(internalInvoke:) target:self argument:[NSArray arrayWithObjects:handler, sender == nil ? [NSNull null] : sender, event == nil ? [NSNull null] : event, nil] order:order++ modes:[NSArray arrayWithObject:NSDefaultRunLoopMode]];
    }
    /* Передача объектов производится через массив для возможности работы с потоками через performSelector:target:argument:order:modes: */
    -(void)internalInvoke:(NSArray *)data
    {
    	AWEventHandler *handler = [data objectAtIndex:0];
    	id sender = [data objectAtIndex:1];
    	if(sender == [NSNull null])
    		sender = nil;
    	id args = [data objectAtIndex:2];
    	if(args == [NSNull null])
    		args = nil;
    	/* Данный класс используется для анализа сигнатуры метода и определения потребного числа параметров его вызова */
    	NSMethodSignature *mSig = [handler.target methodSignatureForSelector:handler.method];
    	if([mSig numberOfArguments] == 2)
    		[handler.target performSelector:handler.method];
    	else if([mSig numberOfArguments] == 3)
    		[handler.target performSelector:handler.method withObject:sender];
    	else if ([mSig numberOfArguments] == 4)
    		[handler.target performSelector:handler.method withObject:sender withObject:args];
    	else
    		@throw [NSException exceptionWithName:@"Invalid selector type" reason:@"This type of selector is not supported" userInfo:nil];
    }
    -(void)dealloc
    {
    	self.name = nil;
    	[self clearReceivers];
    	[_handlers release];
    	[super dealloc];
    }
    @end


    Now we define a couple of auxiliary macros that will give us the opportunity to embed the logic of working with events in the class in just two lines.

    #define DEFINE_EVENT(eventName) \
    	-(void)add ## eventName ## Handler:(id)receiver action:(SEL)action; \
    	-(void)remove ## eventName ## Handler:(id)receiver action:(SEL)action 
    #define DEFINE_EVENT_IMPL(eventName, innerVariable) \
    	-(void)add ## eventName ## Handler:(id)receiver action:(SEL)action \
    	{ \
    		[innerVariable addReceiver:receiver delegate:action]; \
    	} \
    	\
    	-(void)remove ## eventName ## Handler:(id)receiver action:(SEL)action \
    	{ \
    		[innerVariable removeReceiver:receiver delegate:action] ; \
    	} \


    Now, in order to create an event in the class, you need to define an internal list variable:

    AWEventHandlersList *_handlers;


    Define an event in the interface

    DEFINE_EVENT(Event);


    And associate the list with an event

    DEFINE_EVENT_IMPL(Event, _handlers)


    Two methods are automatically added to the class - addEventHandler:action:and removeEventHandler:action:, and you can trigger an event through the methods of the invokeobject _handlers.

    Of course, do not forget that the _handlers object must be initialized in the constructor

    _handlers = [AWEventHandlersList new];


    And destroy in the object destructor

    [_handlers release];


    In the second part of the article I will tell you what problems this approach leads to and how to cope with the difficulties of “dead” links that arise in any more or less voluminous application as a result of our own mistakes.

    Also popular now: