Last active
December 16, 2015 01:49
-
-
Save Goos/5357499 to your computer and use it in GitHub Desktop.
Javascript-style events in cocoa
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// NSObject+EventEmitter.h | |
// Sandbox | |
// | |
// Created by Robin Goos on 4/10/13. | |
// Copyright (c) 2013 Goos. All rights reserved. | |
// | |
#import <Foundation/Foundation.h> | |
typedef void(^Callback)(__weak id slf, NSArray *args); | |
@interface Listener : NSObject | |
@property (nonatomic) SEL selector; | |
@property (nonatomic, copy) Callback cb; | |
@property (nonatomic, strong) NSString *event_type; | |
@property (nonatomic, weak) id caller; | |
@property (nonatomic, weak) id emitter; | |
- (id)initWithCallback:(Callback)cb eventType:(NSString *)type target:(id)target; | |
/** | |
* off: | |
* @POST: | |
Stops the listener from continuing to listen to events. | |
*/ | |
- (void)off; | |
@end | |
@interface NSObject (EventEmitter) | |
/*! Sends out an event that other objects can listen to with on:do:target:. | |
* Expects the variable arguments to be (non-primitive) foundation objects. | |
* Expects the va_list to be terminated with nil. | |
* @param eventType The event type identifier (e.g: "error") | |
* @param ... An optional amount of parameters to be passed with the event (must be terminated with nil). | |
*/ | |
- (void)emit:(NSString *)eventType, ...; | |
/*! Adds a listener to the NSObject, calling the block every time the receiver calls emit:. | |
* The listener is removed either if the listener or the receiver gets deallocated. | |
* @param eventType The event-identifier which determines what event to listen to. | |
* @param callback The block to be called when the event is emitted. See block definition above. | |
* @param target The observer-object that should be the other part of maintaining the responsibility of listening. | |
* | |
* @return An instance of a Listener, in order to stop listening (by calling [listener off];). | |
*/ | |
- (Listener *)on:(NSString *)eventType do:(Callback)callback target:(id)target; | |
/// Same as on:do:with, except that a selector is supplied instead of a callback-block. | |
- (Listener *)on:(NSString *)eventType call:(SEL)selector target:(id)target; | |
@end | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// NSObject+EventEmitter.m | |
// Sandbox | |
// | |
// Created by Robin Goos on 4/10/13. | |
// Copyright (c) 2013 Goos. All rights reserved. | |
// | |
#import <objc/runtime.h> | |
#import "NSObject+EventEmitter.h" | |
static NSString * const kEventTableIdentifier = @"_EmitterEvents"; | |
static NSString * const kListenerIdentifier = @"_CallerListeners"; | |
static NSMutableSet *emitterSwizzledClasses = nil; | |
/** | |
* Helper function - gets / instantiates an associated object for | |
* a listener object | |
*/ | |
NSMutableArray * lazyListenerArray(id target) { | |
id targetArray = objc_getAssociatedObject(target, &kListenerIdentifier); | |
if (![targetArray isKindOfClass:[NSMutableArray class]]) { | |
NSMutableArray *arr = [NSMutableArray array]; | |
objc_setAssociatedObject(target, &kListenerIdentifier, arr, OBJC_ASSOCIATION_RETAIN_NONATOMIC); | |
} | |
return objc_getAssociatedObject(target, &kListenerIdentifier); | |
} | |
/** | |
* Helper function - Returns an array of listeners on the emitter | |
* and caller alike, in order to dereference the listeners. | |
*/ | |
NSArray * objectListeners(id object) { | |
NSMutableArray *listeners = [NSMutableArray array]; | |
NSMutableArray *cListeners = objc_getAssociatedObject(object, &kListenerIdentifier); | |
NSMutableDictionary *eventTable = objc_getAssociatedObject(object, &kEventTableIdentifier); | |
if (eventTable) { | |
for (NSString *key in eventTable) { | |
for (Listener *listener in eventTable[key]) { | |
[listeners addObject:listener]; | |
} | |
} | |
} | |
[listeners addObjectsFromArray:cListeners]; | |
return listeners; | |
} | |
@interface NSObject (_EventEmitter) | |
/** | |
* Event list getter & setter | |
*/ | |
- (NSMutableDictionary *)emitterEvents; | |
- (void)setEmitterEvents:(NSMutableDictionary *)events; | |
- (Listener *)addEventListener:(NSString *)eventType callback:(Callback)callback action:(SEL)selector target:(id)target; | |
- (void)swizzleObjectClass:(id)object; | |
@end | |
#pragma mark - | |
#pragma mark Listener Object | |
@implementation Listener | |
- (id)initWithCallback:(Callback)cb eventType:(NSString *)type target:(id)target | |
{ | |
self = [super init]; | |
if (self) { | |
self.event_type = type; | |
self.caller = target; | |
self.cb = cb; | |
} | |
return self; | |
} | |
- (id)initWithSelector:(SEL)sel eventType:(NSString *)type target:(id)target; | |
{ | |
self = [super init]; | |
if (self) { | |
self.event_type = type; | |
self.caller = target; | |
self.selector = sel; | |
} | |
return self; | |
} | |
- (void)off | |
{ | |
NSMutableArray *callerListeners, *emitterListeners; | |
if (self.caller) { | |
callerListeners = objc_getAssociatedObject(self.caller, &kListenerIdentifier); | |
[callerListeners removeObject:self]; | |
} | |
if (self.emitter) { | |
emitterListeners = objc_getAssociatedObject(self.emitter, &kEventTableIdentifier)[self.event_type]; | |
[emitterListeners removeObject:self]; | |
} | |
} | |
@end | |
#pragma mark - | |
#pragma mark Category Implementation | |
@implementation NSObject (EventEmitter) | |
#pragma mark - | |
#pragma mark Static variable instantiation | |
+ (void)load | |
{ | |
static dispatch_once_t onceToken; | |
dispatch_once(&onceToken, ^{ | |
emitterSwizzledClasses = [NSMutableSet set]; | |
}); | |
} | |
#pragma mark - | |
#pragma mark Getters & Setters | |
- (NSMutableDictionary *)emitterEvents | |
{ | |
return objc_getAssociatedObject(self, &kEventTableIdentifier); | |
} | |
- (void)setEmitterEvents:(NSMutableDictionary *)events | |
{ | |
objc_setAssociatedObject(self, &kEventTableIdentifier, events, OBJC_ASSOCIATION_RETAIN_NONATOMIC); | |
} | |
#pragma mark - | |
#pragma mark Event methods | |
- (void)emit:(NSString *)eventType, ... | |
{ | |
if (!self.emitterEvents) { | |
self.emitterEvents = [[NSMutableDictionary alloc] init]; | |
} | |
NSMutableArray *objArgs = [NSMutableArray array]; | |
va_list args; | |
id obj; | |
va_start(args, eventType); | |
while ((obj = va_arg(args, id)) != nil) { | |
[objArgs addObject:obj]; | |
} | |
va_end(args); | |
id listeners = self.emitterEvents[eventType]; | |
if ([listeners isKindOfClass:[NSMutableArray class]]) { | |
listeners = (NSMutableArray *)listeners; | |
for (Listener *listener in listeners) { | |
__weak typeof(self) this = self; | |
if (listener.selector) { | |
NSMethodSignature *sig = [listener.caller methodSignatureForSelector:listener.selector]; | |
if (sig) { | |
// LLVM warns about the unknown selector as it might not handle the return value, | |
// but it's no problem as the listener-methods don't have return values. | |
#pragma clang diagnostic push | |
#pragma clang diagnostic ignored "-Warc-performSelector-leaks" | |
if (sig.numberOfArguments == 3) { | |
[listener.caller performSelector:listener.selector withObject:self]; | |
} else if (sig.numberOfArguments == 4) { | |
[listener.caller performSelector:listener.selector withObject:self withObject:objArgs]; | |
} else { | |
[listener.caller performSelector:listener.selector]; | |
} | |
#pragma clang diagnostic pop | |
} | |
} else if (listener.cb) { | |
listener.cb(this, objArgs); | |
} | |
} | |
} | |
} | |
- (Listener *)addEventListener:(NSString *)eventType callback:(Callback)callback action:(SEL)selector target:(id)target | |
{ | |
// Either a callback or action must be supplied, as well as target. | |
if ((!callback && !selector) || !target) { | |
return nil; | |
} | |
// Lazy initialization | |
if (!self.emitterEvents) { | |
self.emitterEvents = [[NSMutableDictionary alloc] init]; | |
} | |
if (![self.emitterEvents[eventType] isKindOfClass:[NSArray class]]) { | |
self.emitterEvents[eventType] = [NSMutableArray array]; | |
} | |
NSMutableArray *listeners = self.emitterEvents[eventType]; | |
[self swizzleObjectClass:target]; | |
[self swizzleObjectClass:self]; | |
Listener *listener; | |
if (callback) { | |
listener = [[Listener alloc] initWithCallback:callback eventType:eventType target:target]; | |
} else { | |
listener = [[Listener alloc] initWithSelector:selector eventType:eventType target:target]; | |
} | |
listener.emitter = self; | |
[listeners addObject:listener]; | |
NSMutableArray *arr = lazyListenerArray(target); | |
[arr addObject:listener]; | |
return listener; | |
} | |
- (Listener *)on:(NSString *)eventType do:(Callback)callback target:(id)target | |
{ | |
return [self addEventListener:eventType callback:callback action:nil target:target]; | |
} | |
- (Listener *)on:(NSString *)eventType call:(SEL)selector target:(id)target | |
{ | |
return [self addEventListener:eventType callback:nil action:selector target:target]; | |
} | |
#pragma mark - | |
#pragma mark Method swizzling | |
- (void)swizzleObjectClass:(id)object | |
{ | |
if (!object) | |
return; | |
@synchronized (emitterSwizzledClasses) { | |
Class cl = [object class]; | |
if ([emitterSwizzledClasses containsObject:cl]) | |
return; | |
SEL ds = NSSelectorFromString(@"dealloc"); | |
Method dm = class_getInstanceMethod(cl, ds); | |
IMP oi = method_getImplementation(dm), | |
ni; | |
// In order to stop xcode whining about casting block to pointer | |
#if __IPHONE_OS_VERSION_MAX_ALLOWED < __IPHONE_6_0 || __MAC_OS_X_VERSION_MAX_ALLOWED < __MAC_10_8 | |
ni = imp_implementationWithBlock(^(void *obj) | |
#else | |
ni = imp_implementationWithBlock((__bridge void *)^ (void *obj) | |
#endif | |
{ | |
@autoreleasepool { | |
NSArray *listeners = objectListeners((__bridge id)obj); | |
@synchronized(listeners) { | |
for (Listener *listener in listeners) { | |
[listener off]; | |
} | |
} | |
((void (*)(void *, SEL))oi)(obj, ds); | |
} | |
}); | |
class_replaceMethod(cl, ds, ni, method_getTypeEncoding(dm)); | |
[emitterSwizzledClasses addObject:cl]; | |
} | |
} | |
@end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Usage | |
// hypothetical "news" instance | |
[self emit:@"news", breakingStory, nil]; | |
// hypothetical controller | |
[emitter on:@"news" do:^(id emitter, NSArray *args) { | |
NSLog(@"Breaking news!\n %@", args[0]); | |
} target: self]; | |
// with selectors as well | |
[view on:@"action" call:@selector(someView:firedActionWithArguments:) target:self]; | |
// A hypothetical NSURLConnection subclass | |
EasyRequest *req = [EasyRequest get: | |
[NSURL URLWithString:@"http://google.se"]]; | |
NSMutableData *buffer = [NSMutableData data]; | |
[req on:@"data" do:^(EasyRequest *req, NSArray *args) { | |
[buffer appendData:(NSData*)args[0]]; | |
} target:self]; | |
[req on:@"done" do:^(EasyRequest *req, NSArray *args) { | |
NSString *body = [[NSString alloc] | |
initWithData:buffer | |
encoding:NSASCIIStringEncoding]; | |
NSLog(@"response: %@", body); | |
} target:self]; | |
[req on:@"error" do:^(EasyRequest *req, NSArray *args) { | |
NSLog(@"error: %@", args[0]); | |
} target:self]; | |
// When the req instance deallocates, it will remove the listeners from both "self" and the request. | |
// If you want to manually remove the listener before deallocation, do | |
Listener *listener = [req on:@"data" do^(EasyRequest *req, NSArray *args) { | |
NSLog(@"data: %@", args[0]); | |
} target:self]; | |
[listener off]; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment