-
-
Save m1entus/22ce2149233c4799a4aea856f520564a to your computer and use it in GitHub Desktop.
// | |
// CoreDataContextWatcher.swift | |
// ContextWatcher | |
// | |
// Created by Michal Zaborowski on 10.05.2016. | |
// Copyright © 2016 Inspace Labs Sp z o. o. Spółka Komandytowa. All rights reserved. | |
// | |
import Foundation | |
import CoreData | |
public struct CoreDataContextObserverState: OptionSetType { | |
public let rawValue: Int | |
public init(rawValue: Int) { self.rawValue = rawValue } | |
public static let Inserted = CoreDataContextObserverState(rawValue: 1 << 0) | |
public static let Updated = CoreDataContextObserverState(rawValue: 1 << 1) | |
public static let Deleted = CoreDataContextObserverState(rawValue: 1 << 2) | |
public static let Refreshed = CoreDataContextObserverState(rawValue: 1 << 3) | |
public static let All: CoreDataContextObserverState = [Inserted, Updated, Deleted, Refreshed] | |
} | |
public typealias CoreDataContextObserverCompletionBlock = (NSManagedObject,CoreDataContextObserverState) -> () | |
public typealias CoreDataContextObserverContextChangeBlock = (notification: NSNotification, changedObjects: [CoreDataObserverObjectChange]) -> () | |
public enum CoreDataObserverObjectChange { | |
case Updated(NSManagedObject) | |
case Refreshed(NSManagedObject) | |
case Inserted(NSManagedObject) | |
case Deleted(NSManagedObject) | |
public func managedObject() -> NSManagedObject { | |
switch self { | |
case let .Updated(value): return value | |
case let .Inserted(value): return value | |
case let .Refreshed(value): return value | |
case let .Deleted(value): return value | |
} | |
} | |
} | |
public struct CoreDataObserverAction { | |
var state: CoreDataContextObserverState | |
var completionBlock: CoreDataContextObserverCompletionBlock | |
} | |
public class CoreDataContextObserver { | |
public var enabled: Bool = true | |
public var contextChangeBlock: CoreDataContextObserverContextChangeBlock? | |
private var notificationObserver: NSObjectProtocol? | |
private(set) var context: NSManagedObjectContext | |
private(set) var actionsForManagedObjectID: Dictionary<NSManagedObjectID,[CoreDataObserverAction]> = [:] | |
private(set) weak var persistentStoreCoordinator: NSPersistentStoreCoordinator? | |
deinit { | |
unobserveAllObjects() | |
if let notificationObserver = notificationObserver { | |
NSNotificationCenter.defaultCenter().removeObserver(notificationObserver) | |
} | |
} | |
public init(context: NSManagedObjectContext) { | |
self.context = context | |
self.persistentStoreCoordinator = context.persistentStoreCoordinator | |
notificationObserver = NSNotificationCenter.defaultCenter().addObserverForName(NSManagedObjectContextObjectsDidChangeNotification, object: context, queue: nil) { [weak self] notification in | |
self?.handleContextObjectDidChangeNotification(notification) | |
} | |
} | |
private func handleContextObjectDidChangeNotification(notification: NSNotification) { | |
guard let incomingContext = notification.object as? NSManagedObjectContext, | |
let persistentStoreCoordinator = persistentStoreCoordinator, | |
let incomingPersistentStoreCoordinator = incomingContext.persistentStoreCoordinator | |
where enabled && persistentStoreCoordinator == incomingPersistentStoreCoordinator else { | |
return | |
} | |
let insertedObjectsSet = notification.userInfo?[NSInsertedObjectsKey] as? Set<NSManagedObject> ?? Set<NSManagedObject>() | |
let updatedObjectsSet = notification.userInfo?[NSUpdatedObjectsKey] as? Set<NSManagedObject> ?? Set<NSManagedObject>() | |
let deletedObjectsSet = notification.userInfo?[NSDeletedObjectsKey] as? Set<NSManagedObject> ?? Set<NSManagedObject>() | |
let refreshedObjectsSet = notification.userInfo?[NSRefreshedObjectsKey] as? Set<NSManagedObject> ?? Set<NSManagedObject>() | |
var combinedObjectChanges = insertedObjectsSet.map({ CoreDataObserverObjectChange.Inserted($0) }) | |
combinedObjectChanges += updatedObjectsSet.map({ CoreDataObserverObjectChange.Updated($0) }) | |
combinedObjectChanges += deletedObjectsSet.map({ CoreDataObserverObjectChange.Deleted($0) }) | |
combinedObjectChanges += refreshedObjectsSet.map({ CoreDataObserverObjectChange.Refreshed($0) }) | |
contextChangeBlock?(notification: notification,changedObjects: combinedObjectChanges) | |
let combinedSet = insertedObjectsSet.union(updatedObjectsSet).union(deletedObjectsSet) | |
let allObjectIDs = Array(actionsForManagedObjectID.keys) | |
let filteredObjects = combinedSet.filter({ allObjectIDs.contains($0.objectID) }) | |
for object in filteredObjects { | |
guard let actionsForObject = actionsForManagedObjectID[object.objectID] else { continue } | |
for action in actionsForObject { | |
if action.state.contains(.Inserted) && insertedObjectsSet.contains(object) { | |
action.completionBlock(object,.Inserted) | |
} else if action.state.contains(.Updated) && updatedObjectsSet.contains(object) { | |
action.completionBlock(object,.Updated) | |
} else if action.state.contains(.Deleted) && deletedObjectsSet.contains(object) { | |
action.completionBlock(object,.Deleted) | |
} else if action.state.contains(.Refreshed) && refreshedObjectsSet.contains(object) { | |
action.completionBlock(object,.Refreshed) | |
} | |
} | |
} | |
} | |
public func observeObject(object: NSManagedObject, state: CoreDataContextObserverState = .All, completionBlock: CoreDataContextObserverCompletionBlock) { | |
let action = CoreDataObserverAction(state: state, completionBlock: completionBlock) | |
if var actionArray = actionsForManagedObjectID[object.objectID] { | |
actionArray.append(action) | |
actionsForManagedObjectID[object.objectID] = actionArray | |
} else { | |
actionsForManagedObjectID[object.objectID] = [action] | |
} | |
} | |
public func unobserveObject(object: NSManagedObject, forState state: CoreDataContextObserverState = .All) { | |
if state == .All { | |
actionsForManagedObjectID.removeValueForKey(object.objectID) | |
} else if let actionsForObject = actionsForManagedObjectID[object.objectID] { | |
actionsForManagedObjectID[object.objectID] = actionsForObject.filter({ !$0.state.contains(state) }) | |
} | |
} | |
public func unobserveAllObjects() { | |
actionsForManagedObjectID.removeAll() | |
} | |
} |
Thanks Michal, this is great!
I updated the solution to Swift 3
//
// CoreDataContextObserver.swift
// CoreDataContextObserver
//
// Created by Michal Zaborowski on 10.05.2016.
// Copyright © 2016 Inspace Labs Sp z o. o. Spółka Komandytowa. All rights reserved.
//
import Foundation
import CoreData
public struct CoreDataContextObserverState: OptionSet {
public let rawValue: Int
public init(rawValue: Int) { self.rawValue = rawValue }
public static let inserted = CoreDataContextObserverState(rawValue: 1 << 0)
public static let updated = CoreDataContextObserverState(rawValue: 1 << 1)
public static let deleted = CoreDataContextObserverState(rawValue: 1 << 2)
public static let refreshed = CoreDataContextObserverState(rawValue: 1 << 3)
public static let all: CoreDataContextObserverState = [inserted, updated, deleted, refreshed]
}
public enum CoreDataObserverObjectChange {
case updated(NSManagedObject)
case refreshed(NSManagedObject)
case inserted(NSManagedObject)
case deleted(NSManagedObject)
public func managedObject() -> NSManagedObject {
switch self {
case let .updated(value): return value
case let .inserted(value): return value
case let .refreshed(value): return value
case let .deleted(value): return value
}
}
}
public struct CoreDataObserverAction {
var state: CoreDataContextObserverState
var completionBlock: CoreDataContextObserver.CompletionBlock
}
public class CoreDataContextObserver {
public typealias CompletionBlock = (NSManagedObject, CoreDataContextObserverState) -> ()
public typealias ContextChangeBlock = (_ notification: NSNotification, _ changedObjects: [CoreDataObserverObjectChange]) -> ()
public var enabled: Bool = true
public var contextChangeBlock: CoreDataContextObserver.ContextChangeBlock?
private var notificationObserver: NSObjectProtocol?
private(set) var context: NSManagedObjectContext
private(set) var actionsForManagedObjectID = Dictionary<NSManagedObjectID, [CoreDataObserverAction]>()
private(set) weak var persistentStoreCoordinator: NSPersistentStoreCoordinator?
deinit {
unobserveAllObjects()
if let notificationObserver = notificationObserver {
NotificationCenter.default.removeObserver(notificationObserver)
}
}
public init(context: NSManagedObjectContext) {
self.context = context
self.persistentStoreCoordinator = context.persistentStoreCoordinator
notificationObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.NSManagedObjectContextObjectsDidChange, object: context, queue: nil, using: { notification in
self.handleContextObjectDidChangeNotification(notification: notification as NSNotification)
})
}
private func handleContextObjectDidChangeNotification(notification: NSNotification) {
guard let incomingContext = notification.object as? NSManagedObjectContext,
let persistentStoreCoordinator = persistentStoreCoordinator,
let incomingPersistentStoreCoordinator = incomingContext.persistentStoreCoordinator,
enabled && persistentStoreCoordinator == incomingPersistentStoreCoordinator else {
return
}
let insertedObjectsSet = notification.userInfo?[NSInsertedObjectsKey] as? Set<NSManagedObject> ?? Set<NSManagedObject>()
let updatedObjectsSet = notification.userInfo?[NSUpdatedObjectsKey] as? Set<NSManagedObject> ?? Set<NSManagedObject>()
let deletedObjectsSet = notification.userInfo?[NSDeletedObjectsKey] as? Set<NSManagedObject> ?? Set<NSManagedObject>()
let refreshedObjectsSet = notification.userInfo?[NSRefreshedObjectsKey] as? Set<NSManagedObject> ?? Set<NSManagedObject>()
var combinedObjectChanges = insertedObjectsSet.map({ CoreDataObserverObjectChange.inserted($0) })
combinedObjectChanges += updatedObjectsSet.map({ CoreDataObserverObjectChange.updated($0) })
combinedObjectChanges += deletedObjectsSet.map({ CoreDataObserverObjectChange.deleted($0) })
combinedObjectChanges += refreshedObjectsSet.map({ CoreDataObserverObjectChange.refreshed($0) })
contextChangeBlock?(notification, combinedObjectChanges)
let combinedSet = insertedObjectsSet.union(updatedObjectsSet).union(deletedObjectsSet)
let allObjectIDs = Array(actionsForManagedObjectID.keys)
let filteredObjects = combinedSet.filter({ allObjectIDs.contains($0.objectID) })
for object in filteredObjects {
guard let actionsForObject = actionsForManagedObjectID[object.objectID] else { continue }
for action in actionsForObject {
if action.state.contains(.inserted) && insertedObjectsSet.contains(object) {
action.completionBlock(object, .inserted)
} else if action.state.contains(.updated) && updatedObjectsSet.contains(object) {
action.completionBlock(object, .updated)
} else if action.state.contains(.deleted) && deletedObjectsSet.contains(object) {
action.completionBlock(object, .deleted)
} else if action.state.contains(.refreshed) && refreshedObjectsSet.contains(object) {
action.completionBlock(object, .refreshed)
}
}
}
}
public func observeObject(object: NSManagedObject, state: CoreDataContextObserverState = .all, completionBlock: @escaping CoreDataContextObserver.CompletionBlock) {
let action = CoreDataObserverAction(state: state, completionBlock: completionBlock)
if var actionArray = actionsForManagedObjectID[object.objectID] {
actionArray.append(action)
actionsForManagedObjectID[object.objectID] = actionArray
} else {
actionsForManagedObjectID[object.objectID] = [action]
}
}
public func unobserveObject(object: NSManagedObject, forState state: CoreDataContextObserverState = .all) {
if state == .all {
actionsForManagedObjectID.removeValue(forKey: object.objectID)
} else if let actionsForObject = actionsForManagedObjectID[object.objectID] {
actionsForManagedObjectID[object.objectID] = actionsForObject.filter({ !$0.state.contains(state) })
}
}
public func unobserveAllObjects() {
actionsForManagedObjectID.removeAll()
}
}
Thank you very much for that! Was looking for exactly that.
Because I really like generics and don't want to always cast my NSManagedObject types, I made you implementation generic (based on irpainel's Swift 3 fix).
import Foundation
import CoreData
public struct CoreDataContextObserverState: OptionSet {
public let rawValue: Int
public init(rawValue: Int) { self.rawValue = rawValue }
public static let inserted = CoreDataContextObserverState(rawValue: 1 << 0)
public static let updated = CoreDataContextObserverState(rawValue: 1 << 1)
public static let deleted = CoreDataContextObserverState(rawValue: 1 << 2)
public static let refreshed = CoreDataContextObserverState(rawValue: 1 << 3)
public static let all: CoreDataContextObserverState = [inserted, updated, deleted, refreshed]
}
public enum CoreDataObserverObjectChange {
case updated(NSManagedObject)
case refreshed(NSManagedObject)
case inserted(NSManagedObject)
case deleted(NSManagedObject)
public func managedObject() -> NSManagedObject {
switch self {
case let .updated(value): return value
case let .inserted(value): return value
case let .refreshed(value): return value
case let .deleted(value): return value
}
}
}
public struct CoreDataObserverAction<T:NSManagedObject> {
var state: CoreDataContextObserverState
var completionBlock: (T, CoreDataContextObserverState) -> ()
}
public class CoreDataContextObserver<T:NSManagedObject> {
public typealias CompletionBlock = (NSManagedObject, CoreDataContextObserverState) -> ()
public typealias ContextChangeBlock = (_ notification: NSNotification, _ changedObjects: [CoreDataObserverObjectChange]) -> ()
public var enabled: Bool = true
public var contextChangeBlock: CoreDataContextObserver.ContextChangeBlock?
private var notificationObserver: NSObjectProtocol?
private(set) var context: NSManagedObjectContext
private(set) var actionsForManagedObjectID = Dictionary<NSManagedObjectID, [CoreDataObserverAction<T>]>()
private(set) weak var persistentStoreCoordinator: NSPersistentStoreCoordinator?
deinit {
unobserveAllObjects()
if let notificationObserver = notificationObserver {
NotificationCenter.default.removeObserver(notificationObserver)
}
}
public init(context: NSManagedObjectContext) {
self.context = context
self.persistentStoreCoordinator = context.persistentStoreCoordinator
notificationObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.NSManagedObjectContextObjectsDidChange, object: context, queue: nil, using: { notification in
self.handleContextObjectDidChangeNotification(notification: notification as NSNotification)
})
}
private func handleContextObjectDidChangeNotification(notification: NSNotification) {
guard let incomingContext = notification.object as? NSManagedObjectContext,
let persistentStoreCoordinator = persistentStoreCoordinator,
let incomingPersistentStoreCoordinator = incomingContext.persistentStoreCoordinator,
enabled && persistentStoreCoordinator == incomingPersistentStoreCoordinator else {
return
}
let insertedObjectsSet = notification.userInfo?[NSInsertedObjectsKey] as? Set<NSManagedObject> ?? Set<NSManagedObject>()
let updatedObjectsSet = notification.userInfo?[NSUpdatedObjectsKey] as? Set<NSManagedObject> ?? Set<NSManagedObject>()
let deletedObjectsSet = notification.userInfo?[NSDeletedObjectsKey] as? Set<NSManagedObject> ?? Set<NSManagedObject>()
let refreshedObjectsSet = notification.userInfo?[NSRefreshedObjectsKey] as? Set<NSManagedObject> ?? Set<NSManagedObject>()
var combinedObjectChanges = insertedObjectsSet.map({ CoreDataObserverObjectChange.inserted($0) })
combinedObjectChanges += updatedObjectsSet.map({ CoreDataObserverObjectChange.updated($0) })
combinedObjectChanges += deletedObjectsSet.map({ CoreDataObserverObjectChange.deleted($0) })
combinedObjectChanges += refreshedObjectsSet.map({ CoreDataObserverObjectChange.refreshed($0) })
contextChangeBlock?(notification, combinedObjectChanges)
let combinedSet = insertedObjectsSet.union(updatedObjectsSet).union(deletedObjectsSet).union(refreshedObjectsSet)
let allObjectIDs = Array(actionsForManagedObjectID.keys)
let filteredObjects = combinedSet.filter({ allObjectIDs.contains($0.objectID) })
for case let object as T in filteredObjects {
guard let actionsForObject = actionsForManagedObjectID[object.objectID] else { continue }
for action in actionsForObject {
if action.state.contains(.inserted) && insertedObjectsSet.contains(object) {
action.completionBlock(object, .inserted)
} else if action.state.contains(.updated) && updatedObjectsSet.contains(object) {
action.completionBlock(object, .updated)
} else if action.state.contains(.deleted) && deletedObjectsSet.contains(object) {
action.completionBlock(object, .deleted)
} else if action.state.contains(.refreshed) && refreshedObjectsSet.contains(object) {
action.completionBlock(object, .refreshed)
}
}
}
}
public func observeObject(object: T, state: CoreDataContextObserverState = .all, completionBlock: @escaping (T, CoreDataContextObserverState) -> ()) {
let action = CoreDataObserverAction<T>(state: state, completionBlock: completionBlock)
if var actionArray : [CoreDataObserverAction<T>] = actionsForManagedObjectID[object.objectID] {
actionArray.append(action)
actionsForManagedObjectID[object.objectID] = actionArray
} else {
actionsForManagedObjectID[object.objectID] = [action]
}
}
public func unobserveObject(object: NSManagedObject, forState state: CoreDataContextObserverState = .all) {
if state == .all {
actionsForManagedObjectID.removeValue(forKey: object.objectID)
} else if let actionsForObject = actionsForManagedObjectID[object.objectID] {
actionsForManagedObjectID[object.objectID] = actionsForObject.filter({ !$0.state.contains(state) })
}
}
public func unobserveAllObjects() {
actionsForManagedObjectID.removeAll()
}
}
Usage:
let context = NSManagedObjectContext.defaultContext // or whatever context you are using
let observer = CoreDataContextObserver<MyManagedClass>(context: context)
observer.observeObject(object: objectOfTypeMyManagedClass, state: .updated, completionBlock: {
updatedObject, state in
// updatedObject will be of type MyManagedClass here
})
Awesome 👍 to all of you
Just found a bug:
In func handleContextObjectDidChangeNotification
the combinedSet
does not include the refreshed objects. I updated my generic version.
On the swift 3 version, there is a retain cycle on the following lines
notificationObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.NSManagedObjectContextObjectsDidChange, object: context, queue: nil, using: { notification in
self.handleContextObjectDidChangeNotification(notification: notification as NSNotification)
})
It should be
notificationObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.NSManagedObjectContextObjectsDidChange, object: context, queue: nil, using: { [weak self] notification in
self?.handleContextObjectDidChangeNotification(notification: notification as NSNotification)
})
Thats true, thanks!
it looks great, much better than put RxSwift on my code.
At the top, you have Copyright © 2016 Inspace Labs Sp z o. o. Spółka Komandytowa. All rights reserved.
Seems likely to be accidental since it's publicly posted on a site specifically known for sharing open-source code, but I need to check. Are there any particular license terms for this code?
This is great. You don't happen to have this code in Objective-C, do you?