Welcome to WuJiGu Developer Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
704 views
in Technique[技术] by (71.8m points)

ios - 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")
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

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.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to WuJiGu Developer Q&A Community for programmer and developer-Open, Learning and Share
...