【问题标题】:How to properly use query generation tokens?如何正确使用查询生成令牌?
【发布时间】:2019-12-20 08:35:21
【问题描述】:

我正在尝试使用 CoreData 和 QueryGenerationTokens 工作的示例项目。该项目的本质是将更改提交到计时器上的背景上下文(模拟来自服务器的更改),直到在 UI 上执行操作(例如,按下按钮)才应该显示。

目前,我在后台上下文中保存了更改(每 5 秒添加一个实体并保存)并且它们自动进入视图上下文(如预期的那样,.automaticallyMergesChangesFromParent 设置为 true)。 哪里出了问题,我会在当前查询生成令牌发生任何这些更改之前固定视图上下文。我希望视图不会随着添加的背景项目而更新,但它会随着它们一起更新。所以看起来查询生成令牌没有效果?

我想到的一些可能的问题:

  • 我从 Apple 发现的唯一 example 没有显示他们将其与获取的结果控制器一起使用(我在 SwiftUI 中使用 @FetchRequest,我几乎完全可以肯定它本质上是相同的),所以这可能会产生影响?
  • .automaticallyMergeChangesFromParent 不应使用,我应该尝试合并策略,但这似乎也不起作用,从概念上讲,查询生成令牌似乎应该与此一起使用并固定到生成,无论合并。

视图代码 - 处理从视图上下文加载数据

// Environment object before fetch request necessary
// Passed in wherever main view is instantiated through .environment()
@Environment(\.managedObjectContext) var managedObjectContext

// Acts as fetched results controller, loading data automatically into items upon the managedObjectContext updating
// ExampleCoreDataEntity.retrieveItemsFetchRequest() is an extension method on the entity to easily get a fetch request for the type with sorting
@FetchRequest(fetchRequest: ExampleCoreDataEntity.retrieveItemsFetchRequest()) var items: FetchedResults<ExampleCoreDataEntity>

var body: some View {
    NavigationView {
        // Button to refresh and bring in changes
        Button(
            action: {
                do {
                    try self.managedObjectContext.setQueryGenerationFrom(.current)
                    self.managedObjectContext.refreshAllObjects()
                } catch {
                    print(error.localizedDescription)
                }
            },
            label: { Image(systemName: "arrow.clockwise") }
        )

        // Creates a table of items sorted by the entity itself (entities conform to Hashable)
        List(self.items, id: \.self) { item in
            Text(item.name ?? "")
        }
    }
}

SceneDelegate 中的代码(启动 SwiftUI 应用程序的地方)我还初始化了 CoreData 所需的内容:

// Setup and pass in environment of managed object context to main view 
// via extension on persistent container that sets up CoreData stack
let managedObjectContext = NSPersistentContainer.shared.viewContext
do {
    try managedObjectContext.setQueryGenerationFrom(.current)
} catch {
    print(error.localizedDescription)
}
let view = MainView().environment(\.managedObjectContext, managedObjectContext)

// Setup background adding
timer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(backgroundCode), userInfo: nil, repeats: true)

// Setup window and pass in main view
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: view)

后台添加数据功能:

@objc func backgroundCode() {
    ExampleCoreDataEntity.create(names: ["background object"], in: backgroundContext, shouldSave: true)
}   

NSPersistentContainer 的设置:

extension NSPersistentContainer {

    private struct SharedContainerStorage {
        static let container: NSPersistentContainer = {
            let container = NSPersistentContainer(name: "Core_Data_Exploration")
            container.loadPersistentStores { (description, error) in
                guard error == nil else {
                    assertionFailure("CoreData: Unresolved error \(error!.localizedDescription)")
                    return
                }
                container.viewContext.automaticallyMergesChangesFromParent = true
            }
            return container
        }()
    }

    static var shared: NSPersistentContainer {
        return SharedContainerStorage.container
    }
}

在实体上创建/读取/更新/删除函数:

extension ExampleCoreDataEntity {
    static func retrieveItemsFetchRequest() -> NSFetchRequest<ExampleCoreDataEntity> {
        let request: NSFetchRequest<ExampleCoreDataEntity> = ExampleCoreDataEntity.fetchRequest()
        request.sortDescriptors = [NSSortDescriptor(keyPath: \ExampleCoreDataEntity.creationDate, ascending: false)]
        return request
    }

    static func create(names: [String], in context: NSManagedObjectContext, shouldSave save: Bool = false) {
        context.perform {
            names.forEach { name in
                let item = ExampleCoreDataEntity(context: context)
                item.name = name
                item.creationDate = Date()
                item.identifier = UUID()
            }

            do {
                if save {
                    try context.save()
                }
            } catch {
                // print error
            }
        }
    }

    func delete(in context: NSManagedObjectContext, shouldSave save: Bool = false) {
        context.perform {
            let name = self.name ?? "an item"

            context.delete(context.object(with: self.objectID))
            do {
                if save {
                    try context.save()
                }
            } catch {
                // print error
            }
        }
    }
}

【问题讨论】:

    标签: swift core-data swiftui


    【解决方案1】:

    在您在问题中链接到的文档中,您会看到它说:

    “在任何固定上下文上调用 save()、reset()、mergeChangesFromContextDidSaveNotification: 或 mergeChangesFromRemoteContextSave(:intoContexts:) 将自动将其推进到最新版本以进行操作,然后将其查询生成重置为 currentQueryGenerationToken 。”

    您从后台保存中看到更改的原因是 automaticallyMergesChangesFromParent 只是为了方便 mergeChangesFromContextDidSaveNotification 所以你们这一代正在进步。

    仅供参考,这里是另一个使用查询生成的示例项目 - Synchronizing a Local Store to the Cloud

    这里是相关代码:

    /*
    See LICENSE folder for this sample’s licensing information.
    
    Abstract:
    A class to set up the Core Data stack, observe Core Data notifications, process persistent history, and deduplicate tags.
    */
    
    import Foundation
    import CoreData
    
    // MARK: - Core Data Stack
    
    /**
     Core Data stack setup including history processing.
     */
    class CoreDataStack {
        
        /**
         A persistent container that can load cloud-backed and non-cloud stores.
         */
        lazy var persistentContainer: NSPersistentContainer = {
            
            // Create a container that can load CloudKit-backed stores
            let container = NSPersistentCloudKitContainer(name: "CoreDataCloudKitDemo")
            
            // Enable history tracking and remote notifications
            guard let description = container.persistentStoreDescriptions.first else {
                fatalError("###\(#function): Failed to retrieve a persistent store description.")
            }
            description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
            description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
    
            container.loadPersistentStores(completionHandler: { (_, error) in
                guard let error = error as NSError? else { return }
                fatalError("###\(#function): Failed to load persistent stores:\(error)")
            })
            
            container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
            container.viewContext.transactionAuthor = appTransactionAuthorName
            
            // Pin the viewContext to the current generation token and set it to keep itself up to date with local changes.
            container.viewContext.automaticallyMergesChangesFromParent = true
            do {
                try container.viewContext.setQueryGenerationFrom(.current)
            } catch {
                fatalError("###\(#function): Failed to pin viewContext to the current generation:\(error)")
            }
            
            // Observe Core Data remote change notifications.
            NotificationCenter.default.addObserver(
                self, selector: #selector(type(of: self).storeRemoteChange(_:)),
                name: .NSPersistentStoreRemoteChange, object: container)
            
            return container
        }()
    
        /**
         Track the last history token processed for a store, and write its value to file.
         
         The historyQueue reads the token when executing operations, and updates it after processing is complete.
         */
        private var lastHistoryToken: NSPersistentHistoryToken? = nil {
            didSet {
                guard let token = lastHistoryToken,
                    let data = try? NSKeyedArchiver.archivedData( withRootObject: token, requiringSecureCoding: true) else { return }
                
                do {
                    try data.write(to: tokenFile)
                } catch {
                    print("###\(#function): Failed to write token data. Error = \(error)")
                }
            }
        }
        
        /**
         The file URL for persisting the persistent history token.
        */
        private lazy var tokenFile: URL = {
            let url = NSPersistentContainer.defaultDirectoryURL().appendingPathComponent("CoreDataCloudKitDemo", isDirectory: true)
            if !FileManager.default.fileExists(atPath: url.path) {
                do {
                    try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
                } catch {
                    print("###\(#function): Failed to create persistent container URL. Error = \(error)")
                }
            }
            return url.appendingPathComponent("token.data", isDirectory: false)
        }()
        
        /**
         An operation queue for handling history processing tasks: watching changes, deduplicating tags, and triggering UI updates if needed.
         */
        private lazy var historyQueue: OperationQueue = {
            let queue = OperationQueue()
            queue.maxConcurrentOperationCount = 1
            return queue
        }()
        
        /**
         The URL of the thumbnail folder.
         */
        static var attachmentFolder: URL = {
            var url = NSPersistentContainer.defaultDirectoryURL().appendingPathComponent("CoreDataCloudKitDemo", isDirectory: true)
            url = url.appendingPathComponent("attachments", isDirectory: true)
            
            // Create it if it doesn’t exist.
            if !FileManager.default.fileExists(atPath: url.path) {
                do {
                    try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
    
                } catch {
                    print("###\(#function): Failed to create thumbnail folder URL: \(error)")
                }
            }
            return url
        }()
        
        init() {
            // Load the last token from the token file.
            if let tokenData = try? Data(contentsOf: tokenFile) {
                do {
                    lastHistoryToken = try NSKeyedUnarchiver.unarchivedObject(ofClass: NSPersistentHistoryToken.self, from: tokenData)
                } catch {
                    print("###\(#function): Failed to unarchive NSPersistentHistoryToken. Error = \(error)")
                }
            }
        }
    }
    // MARK: - Notifications
    
    extension CoreDataStack {
        /**
         Handle remote store change notifications (.NSPersistentStoreRemoteChange).
         */
        @objc
        func storeRemoteChange(_ notification: Notification) {
            print("###\(#function): Merging changes from the other persistent store coordinator.")
            
            // Process persistent history to merge changes from other coordinators.
            historyQueue.addOperation {
                self.processPersistentHistory()
            }
        }
    }
    
    /**
     Custom notifications in this sample.
     */
    extension Notification.Name {
        static let didFindRelevantTransactions = Notification.Name("didFindRelevantTransactions")
    }
    
    // MARK: - Persistent history processing
    
    extension CoreDataStack {
        
        /**
         Process persistent history, posting any relevant transactions to the current view.
         */
        func processPersistentHistory() {
            let taskContext = persistentContainer.newBackgroundContext()
            taskContext.performAndWait {
                
                // Fetch history received from outside the app since the last token
                let historyFetchRequest = NSPersistentHistoryTransaction.fetchRequest!
                historyFetchRequest.predicate = NSPredicate(format: "author != %@", appTransactionAuthorName)
                let request = NSPersistentHistoryChangeRequest.fetchHistory(after: lastHistoryToken)
                request.fetchRequest = historyFetchRequest
    
                let result = (try? taskContext.execute(request)) as? NSPersistentHistoryResult
                guard let transactions = result?.result as? [NSPersistentHistoryTransaction],
                      !transactions.isEmpty
                    else { return }
    
                // Post transactions relevant to the current view.
                DispatchQueue.main.async {
                    NotificationCenter.default.post(name: .didFindRelevantTransactions, object: self, userInfo: ["transactions": transactions])
                }
    
                // Deduplicate the new tags.
                var newTagObjectIDs = [NSManagedObjectID]()
                let tagEntityName = Tag.entity().name
    
                for transaction in transactions where transaction.changes != nil {
                    for change in transaction.changes!
                        where change.changedObjectID.entity.name == tagEntityName && change.changeType == .insert {
                            newTagObjectIDs.append(change.changedObjectID)
                    }
                }
                if !newTagObjectIDs.isEmpty {
                    deduplicateAndWait(tagObjectIDs: newTagObjectIDs)
                }
                
                // Update the history token using the last transaction.
                lastHistoryToken = transactions.last!.token
            }
        }
    }
    
    // MARK: - Deduplicate tags
    
    extension CoreDataStack {
        /**
         Deduplicate tags with the same name by processing the persistent history, one tag at a time, on the historyQueue.
         
         All peers should eventually reach the same result with no coordination or communication.
         */
        private func deduplicateAndWait(tagObjectIDs: [NSManagedObjectID]) {
            // Make any store changes on a background context
            let taskContext = persistentContainer.backgroundContext()
            
            // Use performAndWait because each step relies on the sequence. Since historyQueue runs in the background, waiting won’t block the main queue.
            taskContext.performAndWait {
                tagObjectIDs.forEach { tagObjectID in
                    self.deduplicate(tagObjectID: tagObjectID, performingContext: taskContext)
                }
                // Save the background context to trigger a notification and merge the result into the viewContext.
                taskContext.save(with: .deduplicate)
            }
        }
    
        /**
         Deduplicate a single tag.
         */
        private func deduplicate(tagObjectID: NSManagedObjectID, performingContext: NSManagedObjectContext) {
            guard let tag = performingContext.object(with: tagObjectID) as? Tag,
                let tagName = tag.name else {
                fatalError("###\(#function): Failed to retrieve a valid tag with ID: \(tagObjectID)")
            }
    
            // Fetch all tags with the same name, sorted by uuid
            let fetchRequest: NSFetchRequest<Tag> = Tag.fetchRequest()
            fetchRequest.sortDescriptors = [NSSortDescriptor(key: Schema.Tag.uuid.rawValue, ascending: true)]
            fetchRequest.predicate = NSPredicate(format: "\(Schema.Tag.name.rawValue) == %@", tagName)
            
            // Return if there are no duplicates.
            guard var duplicatedTags = try? performingContext.fetch(fetchRequest), duplicatedTags.count > 1 else {
                return
            }
            print("###\(#function): Deduplicating tag with name: \(tagName), count: \(duplicatedTags.count)")
            
            // Pick the first tag as the winner.
            let winner = duplicatedTags.first!
            duplicatedTags.removeFirst()
            remove(duplicatedTags: duplicatedTags, winner: winner, performingContext: performingContext)
        }
        
        /**
         Remove duplicate tags from their respective posts, replacing them with the winner.
         */
        private func remove(duplicatedTags: [Tag], winner: Tag, performingContext: NSManagedObjectContext) {
            duplicatedTags.forEach { tag in
                defer { performingContext.delete(tag) }
                guard let posts = tag.posts else { return }
                
                for case let post as Post in posts {
                    if let mutableTags: NSMutableSet = post.tags?.mutableCopy() as? NSMutableSet {
                        if mutableTags.contains(tag) {
                            mutableTags.remove(tag)
                            mutableTags.add(winner)
                        }
                    }
                }
            }
        }
    }
    

    【讨论】:

      【解决方案2】:

      问题是container.viewContext.automaticallyMergesChangesFromParent = true

      在使用查询生成令牌时,该属性不能设置为 true。我回到这个问题,在上面记录的NSManagedObjectContext 的标题中发现了这个automaticallyMergesChangesFromParent

      不支持将上下文固定到非当前查询生成时将此属性设置为 YES。

      使其工作的一般流程如下:

      • 将查询生成令牌设置为.current
      • 在视图上下文中调用.refreshAllObjects()
      • 在获取的结果控制器上调用.performFetch()

      这最后一部分违背了我在使用@FetchRequest 的原始问题中输入的代码——目前,我想不出一种方法来手动重新获取它似乎不是非常hacky。为了解决这个问题,我创建了一个中间存储类,其中包含一个采用其委托协议的FetchedResultsController。该商店还采用ObservableObject,它允许SwiftUI 视图在ObservableObject 采用商店中调用objectWillChange.send() 时监听其更改。

      【讨论】:

      • 标题中的那条注释试图说您需要设置自动MergesChangesFromParent true 和 setQueryGenerationFromToken current。
      猜你喜欢
      • 2012-12-23
      • 1970-01-01
      • 1970-01-01
      • 2017-01-22
      • 2016-03-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多