Skip to content

Instantly share code, notes, and snippets.

@m1entus
Last active July 3, 2021 18:08
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save m1entus/22ce2149233c4799a4aea856f520564a to your computer and use it in GitHub Desktop.
Save m1entus/22ce2149233c4799a4aea856f520564a to your computer and use it in GitHub Desktop.
CoreDataContextObserver
//
// 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()
}
}
@inPhilly
Copy link

This is great. You don't happen to have this code in Objective-C, do you?

@irpainel-zz
Copy link

irpainel-zz commented Aug 22, 2017

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()
    }
}

@kmaschke85
Copy link

kmaschke85 commented Jan 31, 2018

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
        })

@adamdahan
Copy link

Awesome 👍 to all of you

@kmaschke85
Copy link

Just found a bug:
In func handleContextObjectDidChangeNotification the combinedSetdoes not include the refreshed objects. I updated my generic version.

@gabrielgava
Copy link

gabrielgava commented Jul 16, 2019

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)
})

@m1entus
Copy link
Author

m1entus commented Jul 17, 2019

Thats true, thanks!

@jbarros35
Copy link

jbarros35 commented Dec 18, 2019

it looks great, much better than put RxSwift on my code.

@RDStewart
Copy link

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?

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