Last active
August 29, 2015 14:03
-
-
Save ffried/bbf83639d4b01d40820d to your computer and use it in GitHub Desktop.
FFObserver
This file contains 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
// | |
// 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 |
This file contains 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
// | |
// 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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Requires
NSOperationQueue+FFAdditions
which can be found here