Skip to content

Instantly share code, notes, and snippets.

@ffried
Last active August 29, 2015 14:03
Show Gist options
  • Save ffried/bbf83639d4b01d40820d to your computer and use it in GitHub Desktop.
Save ffried/bbf83639d4b01d40820d to your computer and use it in GitHub Desktop.
FFObserver
//
// FFObserver.h
//
// Created by Florian Friedrich on 14.10.13.
// Copyright (c) 2013 Florian Friedrich. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
#import <Foundation/Foundation.h>
@class FFObserver;
/**
* The block which will be called whenever a change is observed.
*
* @param observer The observer which observed the change.
* @param object The object which changed.
* @param keyPath The keyPath which changed.
* @param changeDictionary The change dictionary.
* @see NSObject#observeValueForKeyPath:ofObject:change:context:
*/
typedef void (^FFObserverBlock)(FFObserver *observer, id object, NSString *keyPath, NSDictionary *changeDictionary);
/**
* Handles KVO with ease.
*/
@interface FFObserver : NSObject
#pragma mark - Properties
/**
* The block which will be called whenever a change is observed.
* Notice: This block is always set, even with target/selector initialization.
*/
@property (nonatomic, copy, readonly) FFObserverBlock block;
/**
* The target for the selector.
* Only set if the observer was created with the corresponding initializer / class method.
*/
@property (nonatomic, weak, readonly) id target;
/**
* The selector which will be called on the target.
* Only set if the observer was created with the corresponding initializer / class method.
*/
@property (nonatomic, assign, readonly) SEL selector;
/**
* The observed object.
*/
@property (nonatomic, weak, readonly) id observedObject;
/**
* The observed keypaths.
*/
@property (nonatomic, copy, readonly) NSArray *keyPaths;
/**
* The observed keypath. Nil if multiple keypaths are observed.
*/
@property (nonatomic, copy, readonly) NSString *keyPath;
/**
* The queue on which the callback will happen.
*/
@property (nonatomic, strong) NSOperationQueue *queue;
#pragma mark - Class Methods
#pragma mark Single KeyPath
/**
* Creates an observer and registers it.
* @param object The object to observe. Must not be nil.
* @param keyPath The keyPath to observe on the object. Must not be nil.
* @param queue The queue on which the callback should happen. Will be mainQueue if nil is passed.
* @param block The block to call on observed changes.
* @return A new FFObserver instance.
*/
+ (instancetype)observerWithObject:(id)object
keyPath:(NSString *)keyPath
queue:(NSOperationQueue *)queue
block:(FFObserverBlock)block;
/**
* Creates an observer and registers it.
* @param object The object to observe. Must not be nil.
* @param keyPath The keyPath to observe on the object. Must not be nil.
* @param target The target which should be notified about observed changes. Must not be nil.
* @param selector The selector to call on the target. Must not be nil.
* @param queue The queue on which the callback should happen. Will be mainQueue if nil is passed.
* @return A new FFObserver instance.
*/
+ (instancetype)observerWithObject:(id)object
keyPath:(NSString *)keyPath
target:(id)target
selector:(SEL)selector
queue:(NSOperationQueue *)queue;
#pragma mark Multiple KeyPaths
/**
* Creates an observer and registers it.
* @param object The object to observe. Must not be nil.
* @param keyPaths The keyPaths to observe on the object. Must not be nil.
* @param queue The queue on which the callback should happen. Will be mainQueue if nil is passed.
* @param block The block to call on observed changes.
* @return A new FFObserver instance.
*/
+ (instancetype)observerWithObject:(id)object
keyPaths:(NSArray *)keyPaths
queue:(NSOperationQueue *)queue
block:(FFObserverBlock)block;
/**
* Creates an observer and registers it.
* @param object The object to observe. Must not be nil.
* @param keyPaths The keyPaths to observe on the object. Must not be nil.
* @param target The target which should be notified about observed changes. Must not be nil.
* @param selector The selector to call on the target. Must not be nil.
* @param queue The queue on which the callback should happen. Will be mainQueue if nil is passed.
* @return A new FFObserver instance.
*/
+ (instancetype)observerWithObject:(id)object
keyPaths:(NSArray *)keyPaths
target:(id)target
selector:(SEL)selector
queue:(NSOperationQueue *)queue;
#pragma mark - Initializers
#pragma mark Single KeyPath
/**
* Creates an observer and registers it.
* @param object The object to observe. Must not be nil.
* @param keyPath The keyPath to observe on the object. Must not be nil.
* @param queue The queue on which the callback should happen. Will be mainQueue if nil is passed.
* @param block The block to call on observed changes.
* @return A new FFObserver instance.
*/
- (instancetype)initWithObject:(id)object
keyPath:(NSString *)keyPath
queue:(NSOperationQueue *)queue
block:(FFObserverBlock)block;
/**
* Creates an observer and registers it.
* @param object The object to observe. Must not be nil.
* @param keyPath The keyPath to observe on the object. Must not be nil.
* @param target The target which should be notified about observed changes. Must not be nil.
* @param selector The selector to call on the target. Must not be nil.
* @param queue The queue on which the callback should happen. Will be mainQueue if nil is passed.
* @return A new FFObserver instance.
*/
- (instancetype)initWithObject:(id)object
keyPath:(NSString *)keyPath
target:(id)target
selector:(SEL)selector
queue:(NSOperationQueue *)queue;
#pragma mark Multiple KeyPaths
/**
* Creates an observer and registers it.
* @param object The object to observe. Must not be nil.
* @param keyPaths The keyPaths to observe on the object. Must not be nil.
* @param queue The queue on which the callback should happen. Will be mainQueue if nil is passed.
* @param block The block to call on observed changes.
* @return A new FFObserver instance.
*/
- (instancetype)initWithObject:(id)object
keyPaths:(NSArray *)keyPaths
queue:(NSOperationQueue *)queue
block:(FFObserverBlock)block NS_DESIGNATED_INITIALIZER;
/**
* Creates an observer and registers it.
* @param object The object to observe. Must not be nil.
* @param keyPaths The keyPaths to observe on the object. Must not be nil.
* @param target The target which should be notified about observed changes. Must not be nil.
* @param selector The selector to call on the target. Must not be nil.
* @param queue The queue on which the callback should happen. Will be mainQueue if nil is passed.
* @return A new FFObserver instance.
*/
- (instancetype)initWithObject:(id)object
keyPaths:(NSArray *)keyPaths
target:(id)target
selector:(SEL)selector
queue:(NSOperationQueue *)queue;
@end
#pragma mark - Deprecated
@interface FFObserver (FFDeprecatedMethods)
/**
* Creates an observer and registers it.
* @param object The object to observe. Must not be nil.
* @param keyPath The keyPath to observe on the object. Must not be nil.
* @param block The block to call on observed changes.
* @param queue The queue on which the callback should happen. Will be mainQueue if nil is passed.
* @return A new FFObserver instance.
*/
+ (instancetype)observerWithObject:(id)object
keyPath:(NSString *)keyPath
block:(FFObserverBlock)block
queue:(NSOperationQueue *)queue
__deprecated_msg("Use obesrverWithObject:keyPath:queue:block: instead");
/**
* Creates an observer and registers it.
* @param object The object to observe. Must not be nil.
* @param keyPath The keyPath to observe on the object. Must not be nil.
* @param target The target which should be notified about observed changes. Must not be nil.
* @param selector The selector to call on the target. Must not be nil.
* @return A new FFObserver instance.
*/
+ (instancetype)observerWithObject:(id)object
keyPath:(NSString *)keyPath
target:(id)target
selector:(SEL)selector
__deprecated_msg("Use observerWithObject:keyPath:target:selector:queue: instead");
/**
* Creates an observer and registers it.
* @param object The object to observe. Must not be nil.
* @param keyPath The keyPath to observe on the object. Must not be nil.
* @param block The block to call on observed changes.
* @param queue The queue on which the callback should happen. Will be mainQueue if nil is passed.
* @return A new FFObserver instance.
*/
- (instancetype)initWithObject:(id)object
keyPath:(NSString *)keyPath
block:(FFObserverBlock)block
queue:(NSOperationQueue *)queue
__deprecated_msg("Use initWithObject:keyPath:queue:block: instead");
/**
* Creates an observer and registers it.
* @param object The object to observe. Must not be nil.
* @param keyPath The keyPath to observe on the object. Must not be nil.
* @param target The target which should be notified about observed changes. Must not be nil.
* @param selector The selector to call on the target. Must not be nil.
* @return A new FFObserver instance.
*/
- (instancetype)initWithObject:(id)object
keyPath:(NSString *)keyPath
target:(id)target
selector:(SEL)selector
__deprecated_msg("Use initWithObject:keyPath:target:selector:queue: instead");
/**
* Removes the FFObserver as KVO observer from the observed object.
*/
- (void)removeObserverFromObservingObject __deprecated_msg("Don't use this method, it breaks the internal handling!");
@end
//
// FFObserver.m
//
// Created by Florian Friedrich on 14.10.13.
// Copyright (c) 2013 Florian Friedrich. All rights reserved.
//
#import "FFObserver.h"
#import "NSOperationQueue+FFAdditions.h"
#import <objc/runtime.h>
@interface NSObject (FFObserver)
@property (nonatomic, readonly) BOOL hasObservers;
@property (nonatomic, strong, readonly) NSMutableArray *ffobservers;
- (void)addFFObserver:(FFObserver *)observer;
- (void)removeFFObserver:(FFObserver *)observer;
@end
#define FF_CONTEXT (__bridge void *)(self)
static NSKeyValueObservingOptions const FFObserverOptions = (NSKeyValueObservingOptionOld |
NSKeyValueObservingOptionNew);
@interface FFObserver ()
@property (nonatomic) BOOL didRemoveAsObserver;
@end
@implementation FFObserver
#pragma mark - Class Methods
+ (instancetype)observerWithObject:(id)object
keyPath:(NSString *)keyPath
queue:(NSOperationQueue *)queue
block:(FFObserverBlock)block {
return [[self alloc] initWithObject:object keyPath:keyPath queue:queue block:block];
}
+ (instancetype)observerWithObject:(id)object
keyPaths:(NSArray *)keyPaths
queue:(NSOperationQueue *)queue
block:(FFObserverBlock)block {
return [[self alloc] initWithObject:object keyPaths:keyPaths queue:queue block:block];
}
+ (instancetype)observerWithObject:(id)object
keyPath:(NSString *)keyPath
target:(id)target
selector:(SEL)selector
queue:(NSOperationQueue *)queue {
return [[self alloc] initWithObject:object keyPath:keyPath target:target selector:selector queue:queue];
}
+ (instancetype)observerWithObject:(id)object
keyPaths:(NSArray *)keyPaths
target:(id)target
selector:(SEL)selector
queue:(NSOperationQueue *)queue {
return [[self alloc] initWithObject:object keyPaths:keyPaths target:target selector:selector queue:queue];
}
#pragma mark - Initializers
- (instancetype)initWithObject:(id)object
keyPaths:(NSArray *)keyPaths
queue:(NSOperationQueue *)queue
block:(FFObserverBlock)block {
NSParameterAssert(object);
NSParameterAssert(keyPaths);
self = [super init];
if (self) {
_observedObject = object;
_keyPaths = keyPaths;
_block = block;
self.queue = queue ?: [NSOperationQueue mainQueue];
[self.keyPaths enumerateObjectsUsingBlock:^(NSString *keyPath, NSUInteger idx, BOOL *stop) {
[object addObserver:self forKeyPath:keyPath options:FFObserverOptions context:FF_CONTEXT];
}];
[object addFFObserver:self];
}
return self;
}
- (instancetype)initWithObject:(id)object
keyPath:(NSString *)keyPath
queue:(NSOperationQueue *)queue
block:(FFObserverBlock)block {
NSParameterAssert(keyPath);
return [self initWithObject:object keyPaths:@[keyPath] queue:queue block:block];
}
- (instancetype)initWithObject:(id)object
keyPath:(NSString *)keyPath
target:(id)target
selector:(SEL)selector
queue:(NSOperationQueue *)queue {
NSParameterAssert(keyPath);
return [self initWithObject:object keyPaths:@[keyPath] target:target selector:selector queue:queue];
}
- (instancetype)initWithObject:(id)object
keyPaths:(NSArray *)keyPaths
target:(id)target
selector:(SEL)selector
queue:(NSOperationQueue *)queue {
NSParameterAssert(target);
NSParameterAssert(selector);
self = [self initWithObject:object keyPaths:keyPaths queue:queue block:nil];
if (self) {
// Define the missing vars
_target = target;
_selector = selector;
// Set block
__weak __typeof(self) welf = self;
_block = ^(FFObserver *observer, id object, NSString *keyPath, NSDictionary *changeDictionary) {
__strong __typeof(welf) sself = welf;
// If target responds to selector
if ([sself.target respondsToSelector:sself.selector]) {
// Create NSMethodSignature from selector
NSMethodSignature *signature = [sself.target methodSignatureForSelector:sself.selector];
if (signature) { // If it really exists
// Create invocation from method signature
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
// Set target and selector
invocation.target = sself.target;
invocation.selector = sself.selector;
NSUInteger args = signature.numberOfArguments; // Get number of arguments
// If target wants change dictionary -> add it as argument
if (args == 3) { [invocation setArgument:&changeDictionary atIndex:2]; }
[invocation invoke]; // Invoke the invocation
}
}
};
}
return self;
}
#pragma mark - Deallocation
- (void)dealloc {
if (self.observedObject != nil) {
[self removeObserverFromObservedObject:self.observedObject];
}
}
#pragma mark - Properties
- (NSString *)keyPath {
return (self.keyPaths.count == 1) ? [self.keyPaths firstObject] : nil;
}
#pragma mark - KVO
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if (context == FF_CONTEXT) {
if (self.block != nil) {
__weak __typeof(self) welf = self;
[self.queue addOperationWithBlock:^{
__strong __typeof(welf) sself = welf;
if (sself.block != nil) {
sself.block(sself, object, keyPath, change);
}
} waitUntilFinished:!self.queue.isCurrentQueue];
}
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
- (void)removeObserverFromObservedObject:(id)observedObject {
if (self.observedObject != nil) {
NSAssert(self.observedObject == observedObject,
@"Wrong object passed into %@", NSStringFromSelector(_cmd));
}
if (self.didRemoveAsObserver == NO) {
[self.keyPaths enumerateObjectsUsingBlock:^(NSString *keyPath, NSUInteger idx, BOOL *stop) {
[observedObject removeObserver:self forKeyPath:keyPath context:FF_CONTEXT];
}];
[observedObject removeFFObserver:self];
self.didRemoveAsObserver = YES;
}
}
@end
#pragma mark - Categories
@implementation FFObserver (FFDeprecatedMethods)
+ (instancetype)observerWithObject:(id)object
keyPath:(NSString *)keyPath
block:(FFObserverBlock)block
queue:(NSOperationQueue *)queue {
return [self observerWithObject:object keyPath:keyPath queue:queue block:block];
}
+ (instancetype)observerWithObject:(id)object
keyPath:(NSString *)keyPath
target:(id)target
selector:(SEL)selector {
return [[self alloc] initWithObject:object keyPath:keyPath target:target selector:selector];
}
- (instancetype)initWithObject:(id)object
keyPath:(NSString *)keyPath
block:(FFObserverBlock)block
queue:(NSOperationQueue *)queue {
return [self initWithObject:object keyPath:keyPath queue:queue block:block];
}
- (instancetype)initWithObject:(id)object keyPath:(NSString *)keyPath target:(id)target selector:(SEL)selector {
return [self initWithObject:object keyPath:keyPath target:target selector:selector queue:nil];
}
- (void)removeObserverFromObservingObject {
[self removeObserverFromObservedObject:self.observedObject];
}
@end
@implementation NSObject (FFObservable)
- (NSMutableArray *)ffobservers {
NSMutableArray *mutableObservers = objc_getAssociatedObject(self, _cmd);
if (!mutableObservers) {
mutableObservers = [NSMutableArray array];
objc_setAssociatedObject(self, _cmd, mutableObservers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return mutableObservers;
}
- (BOOL)hasObservers {
return objc_getAssociatedObject(self, @selector(ffobservers)) != nil;
}
- (void)addFFObserver:(FFObserver *)observer {
[self.ffobservers addObject:observer];
}
- (void)removeFFObserver:(FFObserver *)observer {
[self.ffobservers removeObject:observer];
}
- (void)ff_tearDownObservers {
if (!self.hasObservers) return;
NSArray *observers = [NSArray arrayWithArray:self.ffobservers];
[observers enumerateObjectsUsingBlock:^(FFObserver *observer, NSUInteger idx, BOOL *stop) {
[observer removeObserverFromObservedObject:self];
}];
[self.ffobservers removeAllObjects];
}
+ (void)load {
static dispatch_once_t FF_KVOSwizzlingToken;
dispatch_once(&FF_KVOSwizzlingToken, ^{
Class class = [self class];
SEL originalSelector = NSSelectorFromString(@"dealloc");
SEL swizzledSelector = @selector(ff_dealloc);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL didAddMethod = class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
- (void)ff_dealloc {
[self ff_tearDownObservers];
[self ff_dealloc];
}
@end
@ffried
Copy link
Author

ffried commented Jul 10, 2014

Requires NSOperationQueue+FFAdditions which can be found here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment