blob: 604c580307706b85321d524df1633c6ae74762d2 [file] [log] [blame]
//
// SMContextObserverController.m
// StoreMad
//
// Created by Andrew Smith on 3/13/14.
// Copyright (c) 2012 Andrew B. Smith ( http://github.com/drewsmits ). 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 "SMContextObserverController.h"
#import "NSManagedObject+StoreMad.h"
#pragma mark - SMContextObserver
typedef void (^SMContextObserverBlock)(NSSet *updateObjects, NSSet *insertedOjects, NSSet *deletedObjects);
@interface SMContextObserver : NSObject
@property (nonatomic, strong) NSManagedObjectContext *context;
@property (nonatomic, strong) NSPredicate *predicate;
@property (nonatomic, strong) NSString *notificationType;
@property (nonatomic, strong) NSOperationQueue *queue;
@property (nonatomic, copy) SMContextObserverBlock workBlock;
@end
@implementation SMContextObserver
- (void)dealloc
{
[self stopObservingNotifications];
}
#pragma mark - Notifications
- (void)startObservingNotifications
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(notificationDidFire:)
name:self.notificationType
object:self.context];
}
- (void)stopObservingNotifications
{
[[NSNotificationCenter defaultCenter] removeObserver:self
name:self.notificationType
object:self.context];
}
- (void)notificationDidFire:(NSNotification *)notification
{
if (_queue) {
__weak SMContextObserver *weakSelf = self;
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
__strong SMContextObserver *strongSelf = weakSelf;
[strongSelf processContextNotification:notification];
}];
[_queue addOperation:op];
} else {
[self processContextNotification:notification];
}
}
- (void)processContextNotification:(NSNotification *)notification
{
// Bail if no work block
if (!self.workBlock) return;
//
// Grab objects from notification. These are standard keys, as defined by docs
//
NSSet *updatedObjects = [[notification userInfo] objectForKey:NSUpdatedObjectsKey];
NSSet *insertedObjects = [[notification userInfo] objectForKey:NSInsertedObjectsKey];
NSSet *deletedObjects = [[notification userInfo] objectForKey:NSDeletedObjectsKey];
//
// Filter sets using predicate
//
if (!self.predicate) self.predicate = [NSPredicate predicateWithFormat:@"1 = 1"];
NSSet *updatedObjectsFiltered = [updatedObjects filteredSetUsingPredicate:self.predicate];
NSSet *insertedObjectsFiltered = [insertedObjects filteredSetUsingPredicate:self.predicate];
NSSet *deletedObjectsFiltered = [deletedObjects filteredSetUsingPredicate:self.predicate];
//
// Bail if no changes
//
if (updatedObjectsFiltered.count == 0
&& insertedObjectsFiltered.count == 0
&& deletedObjectsFiltered.count == 0) return;
//
// Perform work
//
self.workBlock(updatedObjectsFiltered, insertedObjectsFiltered, deletedObjectsFiltered);
}
@end
#pragma mark - SMContextObserverController
@interface SMContextObserverController ()
@property (nonatomic, strong) NSMutableArray *observers;
@end
@implementation SMContextObserverController
- (id)init
{
self = [super init];
if (self) {
_observers = [NSMutableArray array];
}
return self;
}
#pragma mark - Singleton
+ (instancetype)defaultController
{
static SMContextObserverController *_defaultController = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_defaultController = [[self alloc] init];
});
return _defaultController;
}
#pragma mark - API
- (id)addContextObserverForContext:(NSManagedObjectContext *)context
predicate:(NSPredicate *)predicate
contextNotificationType:(NSString *)notificationType
queue:(NSOperationQueue *)queue
workBlock:(void (^)(NSSet *updateObjects,
NSSet *insertedOjects,
NSSet *deletedObjects))workBlock
{
//
// Validate context
//
NSAssert(context, @"Tried to add an observer to a nil NSManagedObjectContext!");
if (!context) return nil;
//
// Validate notification type
//
NSArray *validNotifications = @[NSManagedObjectContextDidSaveNotification,
NSManagedObjectContextWillSaveNotification,
NSManagedObjectContextObjectsDidChangeNotification];
BOOL valid = [validNotifications containsObject:notificationType];
NSAssert(valid, @"Notification type must be one of the following: NSManagedObjectContextDidSaveNotification, NSManagedObjectContextWillSaveNotification, NSManagedObjectContextObjectsDidChangeNotification");
if (!valid) return nil;
//
// Build observer
//
SMContextObserver *observer = [SMContextObserver new];
observer.context = context;
observer.predicate = predicate;
observer.notificationType = notificationType;
observer.queue = queue;
observer.workBlock = workBlock;
//
// Turn on the observer
//
[observer startObservingNotifications];
//
// Keep track of observer
//
@synchronized (_observers) {
[_observers addObject:observer];
}
return observer;
}
- (id)addContextObserverForChangesToObject:(NSManagedObject *)object
workBlock:(void (^)(NSManagedObject *object))workBlock
{
return [self addContextObserverForChangesToObject:object
queue:nil
workBlock:workBlock];
}
- (id)addContextObserverForChangesToObject:(NSManagedObject *)object
queue:(NSOperationQueue *)queue
workBlock:(void (^)(NSManagedObject *object))workBlock
{
NSAssert(object.stm_hasBeenSaved, @"Object must be saved first to observe. Right now it only has a temporary objectID");
//
// Search for the object we pushed in
//
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"objectID == %@", object.objectID];;
//
// Wrap the work block.
//
SMContextObserverBlock newWorkBlock = ^(NSSet *updateObjects,
NSSet *insertedOjects,
NSSet *deletedObjects) {
NSManagedObject *fetchedObject;
if (updateObjects.count > 0) {
fetchedObject = [updateObjects anyObject];
} else if (insertedOjects.count > 0) {
fetchedObject = [insertedOjects anyObject];
} else if (deletedObjects.count > 0) {
fetchedObject = [deletedObjects anyObject];
}
if (workBlock) {
workBlock(fetchedObject);
}
};
//
// Build the observer
//
id observer = [self addContextObserverForContext:object.managedObjectContext
predicate:predicate
contextNotificationType:NSManagedObjectContextObjectsDidChangeNotification
queue:queue
workBlock:newWorkBlock];
return observer;
}
- (void)removeContextObserver:(SMContextObserver *)observer
{
[observer stopObservingNotifications];
@synchronized (_observers) {
[_observers removeObject:observer];
}
}
@end