Vlad Vlad - 1 year ago 63
iOS Question

CoreData: Get notified when NSManagedObject is changed without keeping reference to NSManagedObject

I want to observe changes of particular

NSManagedObject
and update UI accordingly.

I don't want to keep reference to
NSManagedObject
because it might be removed at any time (i.e. by result of remote push notification).

At the moment I'm setting up
NSFetchRequest
,
NSFetchedResultsController
and
NSFetchedResultsControllerDelegate
to achieve this. But want to simplify this solution (see below).

Is there any simple way to observe changes in
NSManagedObject
without using
NSFetchedResultsControllerDelegate
?

Thank you!

Sample code (Xcode Playground)


import PlaygroundSupport
import Cocoa
import CoreData

PlaygroundPage.current.needsIndefiniteExecution = true

extension NSManagedObject {

public static var entityName: String {
let className = String(describing: self)
return className.components(separatedBy: ".").last!
}

public convenience init(in context: NSManagedObjectContext) throws {
let entityName = type(of: self).entityName
guard let entityDescription = NSEntityDescription.entity(forEntityName: entityName, in: context) else {
fatalError()
}
self.init(entity: entityDescription, insertInto: context)
}
}

@objc(UserInfoEntity)
class UserInfoEntity: NSManagedObject {

@NSManaged var id: Int64
@NSManaged var name: String

convenience init(id: Int64, name: String, in context: NSManagedObjectContext) throws {
try self.init(in: context)
self.id = id
self.name = name
}
}

class DBStack {

static let shared = DBStack()
static var mainContext: NSManagedObjectContext {
return shared.mainContext
}

private typealias PSC = NSPersistentStoreCoordinator
private lazy var coordinator: PSC = PSC(managedObjectModel: self.model)
private lazy var model: NSManagedObjectModel = self.setupModel()
private lazy var writerContext: NSManagedObjectContext = self.setupWriterContext()
private lazy var mainContext: NSManagedObjectContext = self.setupMainContext()
private var isInitialized = false

init() {
}

func setupInMemoryStore() throws {
guard !isInitialized else { return }
isInitialized = true
try coordinator.addPersistentStore(ofType: NSInMemoryStoreType,
configurationName: nil, at: nil, options: nil)
}

static func makeChildContext() -> NSManagedObjectContext {
let moc = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
moc.parent = mainContext
return moc
}

private func setupWriterContext() -> NSManagedObjectContext {
let moc = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
moc.persistentStoreCoordinator = coordinator
return moc
}

private func setupMainContext() -> NSManagedObjectContext {
let moc = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
moc.parent = writerContext
return moc
}

private func setupModel() -> NSManagedObjectModel {

let attributeID = NSAttributeDescription()
attributeID.name = "id"
attributeID.attributeType = .integer64AttributeType
attributeID.isOptional = false
attributeID.isIndexed = true

let attributeName = NSAttributeDescription()
attributeName.name = "name"
attributeName.attributeType = .stringAttributeType
attributeName.isOptional = false

let entityUserInfo = NSEntityDescription()
entityUserInfo.name = "UserInfoEntity"
entityUserInfo.managedObjectClassName = "UserInfoEntity"
entityUserInfo.properties = [attributeID, attributeName]

let model = NSManagedObjectModel()
model.entities = [entityUserInfo]
return model
}
}

class FetchedResultsDelegate: NSObject, NSFetchedResultsControllerDelegate {

public var entityChanged: ((Void) -> Void)?

public func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
entityChanged?() // Notify about content change.
}
}

// Create or Update user info.
func updateUserInfo(id: Int64, name: String) {
let privateContext = DBStack.makeChildContext()
privateContext.perform {
let request: NSFetchRequest<UserInfoEntity> = NSFetchRequest(entityName: UserInfoEntity.entityName)
request.predicate = NSPredicate(format: "%K == %@", argumentArray: [#keyPath(UserInfoEntity.id), id])
request.fetchLimit = 1
do {
if let userInfo = try privateContext.fetch(request).first {
userInfo.name = name
} else {
_ = try UserInfoEntity(id: id, name: name, in: privateContext)
}
if privateContext.hasChanges {
print("→ Will save userInfo. Name: " + name)
try privateContext.save()
}
} catch {
print(error)
}
}
}

let stack = DBStack()
try stack.setupInMemoryStore()
let userID: Int64 = 1
let request: NSFetchRequest<UserInfoEntity> = NSFetchRequest(entityName: UserInfoEntity.entityName)
request.predicate = NSPredicate(format: "%K == %@", argumentArray: [#keyPath(UserInfoEntity.id), userID])
request.sortDescriptors = [NSSortDescriptor(key: #keyPath(UserInfoEntity.name), ascending: false)]
let delegate = FetchedResultsDelegate()
let fetchedResultsController: NSFetchedResultsController<UserInfoEntity>
= NSFetchedResultsController(fetchRequest: request, managedObjectContext: DBStack.mainContext,
sectionNameKeyPath: nil, cacheName: nil)
fetchedResultsController.delegate = delegate

// Here is our event handler. Called on main thread.
delegate.entityChanged = { [weak fetchedResultsController] in
let userInfo = fetchedResultsController?.fetchedObjects?.first
print("! UserInfo changed: \(String(describing: userInfo?.name))")
// Update UI.
}

try fetchedResultsController.performFetch()

DispatchQueue.global().async {
updateUserInfo(id: userID, name: "Alex")
updateUserInfo(id: userID, name: "Alexander")
}


Will print:

→ Will save userInfo. Name: Alex
! UserInfo changed: Optional("Alex")
→ Will save userInfo. Name: Alexander
! UserInfo changed: Optional("Alexander")

Answer Source

One way would be:

  1. Save the value of the objectID property of the managed object you want to watch, instead of a referee to the managed object.
  2. Use NotificationCenter to add an observer for the NSManagedObjectContextObjectsDidChange notification, which is generated by your managed object context.
  3. When you receive this notification, look at the userInfo dictionary for a key called NSUpdatedObjectsKey. It contains references to any managed objects that have changed. See if any of them have the objectID you saved in step 1.

Depending on how you want things to work, you might prefer to use the NSManagedObjectContextDidSave notification instead. You might also want to use NSInsertedObjectsKey and/or NSDeletedObjectsKey.

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download