Skip to content

Instantly share code, notes, and snippets.

@Goos
Last active December 16, 2015 01:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Goos/5357499 to your computer and use it in GitHub Desktop.
Save Goos/5357499 to your computer and use it in GitHub Desktop.
Javascript-style events in cocoa
//
// 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
//
// 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
// 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