Skip to content

Instantly share code, notes, and snippets.

@hankbao
Created May 31, 2015 07:38
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hankbao/0deb0059b837f019f3c9 to your computer and use it in GitHub Desktop.
Save hankbao/0deb0059b837f019f3c9 to your computer and use it in GitHub Desktop.
GCDMulticastDelegate
#import <Foundation/Foundation.h>
@class GCDMulticastDelegateEnumerator;
/**
* This class provides multicast delegate functionality. That is:
* - it provides a means for managing a list of delegates
* - any method invocations to an instance of this class are automatically forwarded to all delegates
*
* For example:
*
* // Make this method call on every added delegate (there may be several)
* [multicastDelegate cog:self didFindThing:thing];
*
* This allows multiple delegates to be added to an xmpp stream or any xmpp module,
* which in turn makes development easier as there can be proper separation of logically different code sections.
*
* In addition, this makes module development easier,
* as multiple delegates can usually be handled in a manner similar to the traditional single delegate paradigm.
*
* This class also provides proper support for GCD queues.
* So each delegate specifies which queue they would like their delegate invocations to be dispatched onto.
*
* All delegate dispatching is done asynchronously (which is a critically important architectural design).
**/
@interface GCDMulticastDelegate : NSObject
- (void)addDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue;
- (void)removeDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue;
- (void)removeDelegate:(id)delegate;
- (void)removeAllDelegates;
- (NSUInteger)count;
- (NSUInteger)countOfClass:(Class)aClass;
- (NSUInteger)countForSelector:(SEL)aSelector;
- (BOOL)hasDelegateThatRespondsToSelector:(SEL)aSelector;
- (GCDMulticastDelegateEnumerator *)delegateEnumerator;
@end
@interface GCDMulticastDelegateEnumerator : NSObject
- (NSUInteger)count;
- (NSUInteger)countOfClass:(Class)aClass;
- (NSUInteger)countForSelector:(SEL)aSelector;
- (BOOL)getNextDelegate:(id *)delPtr delegateQueue:(dispatch_queue_t *)dqPtr;
- (BOOL)getNextDelegate:(id *)delPtr delegateQueue:(dispatch_queue_t *)dqPtr ofClass:(Class)aClass;
- (BOOL)getNextDelegate:(id *)delPtr delegateQueue:(dispatch_queue_t *)dqPtr forSelector:(SEL)aSelector;
@end
#import "GCDMulticastDelegate.h"
#import <libkern/OSAtomic.h>
#if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE
#import <AppKit/AppKit.h>
#endif
#if ! __has_feature(objc_arc)
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
#endif
/**
* How does this class work?
*
* In theory, this class is very straight-forward.
* It provides a way for multiple delegates to be called, each on its own delegate queue.
*
* In other words, any delegate method call to this class
* will get forwarded (dispatch_async'd) to each added delegate.
*
* Important note concerning thread-safety:
*
* This class is designed to be used from within a single dispatch queue.
* In other words, it is NOT thread-safe, and should only be used from within the external dedicated dispatch_queue.
**/
@interface GCDMulticastDelegateNode : NSObject {
@private
#if __has_feature(objc_arc_weak)
__weak id delegate;
#if !TARGET_OS_IPHONE
__unsafe_unretained id unsafeDelegate; // Some classes don't support weak references yet (e.g. NSWindowController)
#endif
#else
__unsafe_unretained id delegate;
#endif
dispatch_queue_t delegateQueue;
}
- (id)initWithDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue;
#if __has_feature(objc_arc_weak)
@property (/* atomic */ readwrite, weak) id delegate;
#if !TARGET_OS_IPHONE
@property (/* atomic */ readwrite, unsafe_unretained) id unsafeDelegate;
#endif
#else
@property (/* atomic */ readwrite, unsafe_unretained) id delegate;
#endif
@property (nonatomic, readonly) dispatch_queue_t delegateQueue;
@end
@interface GCDMulticastDelegate ()
{
NSMutableArray *delegateNodes;
}
- (NSInvocation *)duplicateInvocation:(NSInvocation *)origInvocation;
@end
@interface GCDMulticastDelegateEnumerator ()
{
NSUInteger numNodes;
NSUInteger currentNodeIndex;
NSArray *delegateNodes;
}
- (id)initFromDelegateNodes:(NSMutableArray *)inDelegateNodes;
@end
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@implementation GCDMulticastDelegate
- (id)init
{
if ((self = [super init]))
{
delegateNodes = [[NSMutableArray alloc] init];
}
return self;
}
- (void)addDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue
{
if (delegate == nil) return;
if (delegateQueue == NULL) return;
GCDMulticastDelegateNode *node =
[[GCDMulticastDelegateNode alloc] initWithDelegate:delegate delegateQueue:delegateQueue];
[delegateNodes addObject:node];
}
- (void)removeDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue
{
if (delegate == nil) return;
NSUInteger i;
for (i = [delegateNodes count]; i > 0; i--)
{
GCDMulticastDelegateNode *node = delegateNodes[i - 1];
id nodeDelegate = node.delegate;
#if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE
if (nodeDelegate == [NSNull null])
nodeDelegate = node.unsafeDelegate;
#endif
if (delegate == nodeDelegate)
{
if ((delegateQueue == NULL) || (delegateQueue == node.delegateQueue))
{
// Recall that this node may be retained by a GCDMulticastDelegateEnumerator.
// The enumerator is a thread-safe snapshot of the delegate list at the moment it was created.
// To properly remove this node from list, and from the list(s) of any enumerators,
// we nullify the delegate via the atomic property.
//
// However, the delegateQueue is not modified.
// The thread-safety is hinged on the atomic delegate property.
// The delegateQueue is expected to properly exist until the node is deallocated.
node.delegate = nil;
#if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE
node.unsafeDelegate = nil;
#endif
[delegateNodes removeObjectAtIndex:(i-1)];
}
}
}
}
- (void)removeDelegate:(id)delegate
{
[self removeDelegate:delegate delegateQueue:NULL];
}
- (void)removeAllDelegates
{
for (GCDMulticastDelegateNode *node in delegateNodes)
{
node.delegate = nil;
#if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE
node.unsafeDelegate = nil;
#endif
}
[delegateNodes removeAllObjects];
}
- (NSUInteger)count
{
return [delegateNodes count];
}
- (NSUInteger)countOfClass:(Class)aClass
{
NSUInteger count = 0;
for (GCDMulticastDelegateNode *node in delegateNodes)
{
id nodeDelegate = node.delegate;
#if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE
if (nodeDelegate == [NSNull null])
nodeDelegate = node.unsafeDelegate;
#endif
if ([nodeDelegate isKindOfClass:aClass])
{
count++;
}
}
return count;
}
- (NSUInteger)countForSelector:(SEL)aSelector
{
NSUInteger count = 0;
for (GCDMulticastDelegateNode *node in delegateNodes)
{
id nodeDelegate = node.delegate;
#if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE
if (nodeDelegate == [NSNull null])
nodeDelegate = node.unsafeDelegate;
#endif
if ([nodeDelegate respondsToSelector:aSelector])
{
count++;
}
}
return count;
}
- (BOOL)hasDelegateThatRespondsToSelector:(SEL)aSelector
{
for (GCDMulticastDelegateNode *node in delegateNodes)
{
id nodeDelegate = node.delegate;
#if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE
if (nodeDelegate == [NSNull null])
nodeDelegate = node.unsafeDelegate;
#endif
if ([nodeDelegate respondsToSelector:aSelector])
{
return YES;
}
}
return NO;
}
- (GCDMulticastDelegateEnumerator *)delegateEnumerator
{
return [[GCDMulticastDelegateEnumerator alloc] initFromDelegateNodes:delegateNodes];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
for (GCDMulticastDelegateNode *node in delegateNodes)
{
id nodeDelegate = node.delegate;
#if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE
if (nodeDelegate == [NSNull null])
nodeDelegate = node.unsafeDelegate;
#endif
NSMethodSignature *result = [nodeDelegate methodSignatureForSelector:aSelector];
if (result != nil)
{
return result;
}
}
// This causes a crash...
// return [super methodSignatureForSelector:aSelector];
// This also causes a crash...
// return nil;
return [[self class] instanceMethodSignatureForSelector:@selector(doNothing)];
}
- (void)forwardInvocation:(NSInvocation *)origInvocation
{
SEL selector = [origInvocation selector];
BOOL foundNilDelegate = NO;
for (GCDMulticastDelegateNode *node in delegateNodes)
{
id nodeDelegate = node.delegate;
#if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE
if (nodeDelegate == [NSNull null])
nodeDelegate = node.unsafeDelegate;
#endif
if ([nodeDelegate respondsToSelector:selector])
{
// All delegates MUST be invoked ASYNCHRONOUSLY.
NSInvocation *dupInvocation = [self duplicateInvocation:origInvocation];
dispatch_async(node.delegateQueue, ^{ @autoreleasepool {
[dupInvocation invokeWithTarget:nodeDelegate];
}});
}
else if (nodeDelegate == nil)
{
foundNilDelegate = YES;
}
}
if (foundNilDelegate)
{
// At lease one weak delegate reference disappeared.
// Remove nil delegate nodes from the list.
//
// This is expected to happen very infrequently.
// This is why we handle it separately (as it requires allocating an indexSet).
NSMutableIndexSet *indexSet = [[NSMutableIndexSet alloc] init];
NSUInteger i = 0;
for (GCDMulticastDelegateNode *node in delegateNodes)
{
id nodeDelegate = node.delegate;
#if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE
if (nodeDelegate == [NSNull null])
nodeDelegate = node.unsafeDelegate;
#endif
if (nodeDelegate == nil)
{
[indexSet addIndex:i];
}
i++;
}
[delegateNodes removeObjectsAtIndexes:indexSet];
}
}
- (void)doesNotRecognizeSelector:(SEL)aSelector
{
// Prevent NSInvalidArgumentException
}
- (void)doNothing {}
- (void)dealloc
{
[self removeAllDelegates];
}
- (NSInvocation *)duplicateInvocation:(NSInvocation *)origInvocation
{
NSMethodSignature *methodSignature = [origInvocation methodSignature];
NSInvocation *dupInvocation = [NSInvocation invocationWithMethodSignature:methodSignature];
[dupInvocation setSelector:[origInvocation selector]];
NSUInteger i, count = [methodSignature numberOfArguments];
for (i = 2; i < count; i++)
{
const char *type = [methodSignature getArgumentTypeAtIndex:i];
if (*type == *@encode(BOOL))
{
BOOL value;
[origInvocation getArgument:&value atIndex:i];
[dupInvocation setArgument:&value atIndex:i];
}
else if (*type == *@encode(char) || *type == *@encode(unsigned char))
{
char value;
[origInvocation getArgument:&value atIndex:i];
[dupInvocation setArgument:&value atIndex:i];
}
else if (*type == *@encode(short) || *type == *@encode(unsigned short))
{
short value;
[origInvocation getArgument:&value atIndex:i];
[dupInvocation setArgument:&value atIndex:i];
}
else if (*type == *@encode(int) || *type == *@encode(unsigned int))
{
int value;
[origInvocation getArgument:&value atIndex:i];
[dupInvocation setArgument:&value atIndex:i];
}
else if (*type == *@encode(long) || *type == *@encode(unsigned long))
{
long value;
[origInvocation getArgument:&value atIndex:i];
[dupInvocation setArgument:&value atIndex:i];
}
else if (*type == *@encode(long long) || *type == *@encode(unsigned long long))
{
long long value;
[origInvocation getArgument:&value atIndex:i];
[dupInvocation setArgument:&value atIndex:i];
}
else if (*type == *@encode(double))
{
double value;
[origInvocation getArgument:&value atIndex:i];
[dupInvocation setArgument:&value atIndex:i];
}
else if (*type == *@encode(float))
{
float value;
[origInvocation getArgument:&value atIndex:i];
[dupInvocation setArgument:&value atIndex:i];
}
else if (*type == '@')
{
void *value;
[origInvocation getArgument:&value atIndex:i];
[dupInvocation setArgument:&value atIndex:i];
}
else if (*type == '^')
{
void *block;
[origInvocation getArgument:&block atIndex:i];
[dupInvocation setArgument:&block atIndex:i];
}
else
{
NSString *selectorStr = NSStringFromSelector([origInvocation selector]);
NSString *format = @"Argument %lu to method %@ - Type(%c) not supported";
NSString *reason = [NSString stringWithFormat:format, (unsigned long)(i - 2), selectorStr, *type];
[[NSException exceptionWithName:NSInvalidArgumentException reason:reason userInfo:nil] raise];
}
}
[dupInvocation retainArguments];
return dupInvocation;
}
@end
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@implementation GCDMulticastDelegateNode
@synthesize delegate; // atomic
#if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE
@synthesize unsafeDelegate; // atomic
#endif
@synthesize delegateQueue; // non-atomic
#if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE
static BOOL SupportsWeakReferences(id delegate)
{
// From Apple's documentation:
//
// > Which classes don’t support weak references?
// >
// > You cannot currently create weak references to instances of the following classes:
// >
// > NSATSTypesetter, NSColorSpace, NSFont, NSFontManager, NSFontPanel, NSImage, NSMenuView,
// > NSParagraphStyle, NSSimpleHorizontalTypesetter, NSTableCellView, NSTextView, NSViewController,
// > NSWindow, and NSWindowController.
// >
// > In addition, in OS X no classes in the AV Foundation framework support weak references.
//
// NSMenuView is deprecated (and not available to 64-bit applications).
// NSSimpleHorizontalTypesetter is an internal class.
if ([delegate isKindOfClass:[NSATSTypesetter class]]) return NO;
if ([delegate isKindOfClass:[NSColorSpace class]]) return NO;
if ([delegate isKindOfClass:[NSFont class]]) return NO;
if ([delegate isKindOfClass:[NSFontManager class]]) return NO;
if ([delegate isKindOfClass:[NSFontPanel class]]) return NO;
if ([delegate isKindOfClass:[NSImage class]]) return NO;
if ([delegate isKindOfClass:[NSParagraphStyle class]]) return NO;
if ([delegate isKindOfClass:[NSTableCellView class]]) return NO;
if ([delegate isKindOfClass:[NSTextView class]]) return NO;
if ([delegate isKindOfClass:[NSViewController class]]) return NO;
if ([delegate isKindOfClass:[NSWindow class]]) return NO;
if ([delegate isKindOfClass:[NSWindowController class]]) return NO;
return YES;
}
#endif
- (id)initWithDelegate:(id)inDelegate delegateQueue:(dispatch_queue_t)inDelegateQueue
{
if ((self = [super init]))
{
#if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE
{
if (SupportsWeakReferences(inDelegate))
{
delegate = inDelegate;
delegateQueue = inDelegateQueue;
}
else
{
delegate = [NSNull null];
unsafeDelegate = inDelegate;
delegateQueue = inDelegateQueue;
}
}
#else
{
delegate = inDelegate;
delegateQueue = inDelegateQueue;
}
#endif
#if !OS_OBJECT_USE_OBJC
if (delegateQueue)
dispatch_retain(delegateQueue);
#endif
}
return self;
}
- (void)dealloc
{
#if !OS_OBJECT_USE_OBJC
if (delegateQueue)
dispatch_release(delegateQueue);
#endif
}
@end
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@implementation GCDMulticastDelegateEnumerator
- (id)initFromDelegateNodes:(NSMutableArray *)inDelegateNodes
{
if ((self = [super init]))
{
delegateNodes = [inDelegateNodes copy];
numNodes = [delegateNodes count];
currentNodeIndex = 0;
}
return self;
}
- (NSUInteger)count
{
return numNodes;
}
- (NSUInteger)countOfClass:(Class)aClass
{
NSUInteger count = 0;
for (GCDMulticastDelegateNode *node in delegateNodes)
{
id nodeDelegate = node.delegate;
#if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE
if (nodeDelegate == [NSNull null])
nodeDelegate = node.unsafeDelegate;
#endif
if ([nodeDelegate isKindOfClass:aClass])
{
count++;
}
}
return count;
}
- (NSUInteger)countForSelector:(SEL)aSelector
{
NSUInteger count = 0;
for (GCDMulticastDelegateNode *node in delegateNodes)
{
id nodeDelegate = node.delegate;
#if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE
if (nodeDelegate == [NSNull null])
nodeDelegate = node.unsafeDelegate;
#endif
if ([nodeDelegate respondsToSelector:aSelector])
{
count++;
}
}
return count;
}
- (BOOL)getNextDelegate:(id *)delPtr delegateQueue:(dispatch_queue_t *)dqPtr
{
while (currentNodeIndex < numNodes)
{
GCDMulticastDelegateNode *node = delegateNodes[currentNodeIndex];
currentNodeIndex++;
id nodeDelegate = node.delegate; // snapshot atomic property
#if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE
if (nodeDelegate == [NSNull null])
nodeDelegate = node.unsafeDelegate;
#endif
if (nodeDelegate)
{
if (delPtr) *delPtr = nodeDelegate;
if (dqPtr) *dqPtr = node.delegateQueue;
return YES;
}
}
return NO;
}
- (BOOL)getNextDelegate:(id *)delPtr delegateQueue:(dispatch_queue_t *)dqPtr ofClass:(Class)aClass
{
while (currentNodeIndex < numNodes)
{
GCDMulticastDelegateNode *node = delegateNodes[currentNodeIndex];
currentNodeIndex++;
id nodeDelegate = node.delegate; // snapshot atomic property
#if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE
if (nodeDelegate == [NSNull null])
nodeDelegate = node.unsafeDelegate;
#endif
if ([nodeDelegate isKindOfClass:aClass])
{
if (delPtr) *delPtr = nodeDelegate;
if (dqPtr) *dqPtr = node.delegateQueue;
return YES;
}
}
return NO;
}
- (BOOL)getNextDelegate:(id *)delPtr delegateQueue:(dispatch_queue_t *)dqPtr forSelector:(SEL)aSelector
{
while (currentNodeIndex < numNodes)
{
GCDMulticastDelegateNode *node = delegateNodes[currentNodeIndex];
currentNodeIndex++;
id nodeDelegate = node.delegate; // snapshot atomic property
#if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE
if (nodeDelegate == [NSNull null])
nodeDelegate = node.unsafeDelegate;
#endif
if ([nodeDelegate respondsToSelector:aSelector])
{
if (delPtr) *delPtr = nodeDelegate;
if (dqPtr) *dqPtr = node.delegateQueue;
return YES;
}
}
return NO;
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment