【问题标题】:Is it possible for NSFetchResultsController to perform move and update operation, when we change the section of an item?当我们更改项目的部分时,NSFetchResultsController 是否可以执行移动和更新操作?
【发布时间】:2021-06-24 14:01:33
【问题描述】:

目前,我有一个UICollectionView,它由 2 个部分组成

  1. 固定
  2. 正常

它们看起来如下。


概述

== Pinned ===========
|------|
|NOTE0 |
|------|


== Normal ===========

|------|    |------|
|NOTE1 |    |NOTE2 |
|------|    |------|

|------|
|NOTE3 |
|------|

NSManagedObject

这是NSManagedObject

extension NSPlainNote {

    @nonobjc public class func fetchRequest() -> NSFetchRequest<NSPlainNote> {
        return NSFetchRequest<NSPlainNote>(entityName: "NSPlainNote")
    }

    @NSManaged public var title: String?
    @NSManaged public var body: String?
    @NSManaged public var pinned: Bool
    @NSManaged public var uuid: UUID

}

NSFetchResultsController

我们使用Bool 字段来决定一个项目应该属于Pinned部分还是Normal部分

这就是我们的NSFetchResultsController 的样子

lazy var fetchedResultsController: NSFetchedResultsController<NSPlainNote> = {
    
    // Create a fetch request for the Quake entity sorted by time.
    let fetchRequest = NSFetchRequest<NSPlainNote>(entityName: "NSPlainNote")
    fetchRequest.sortDescriptors = [
        NSSortDescriptor(key: "pinned", ascending: false)
    ]
    
    // Create a fetched results controller and set its fetch request, context, and delegate.
    let controller = NSFetchedResultsController(fetchRequest: fetchRequest,
                                                managedObjectContext: CoreDataStack.INSTANCE.persistentContainer.viewContext,
                                                sectionNameKeyPath: "pinned",
                                                cacheName: nil
    )
    controller.delegate = fetchedResultsControllerDelegate
    
    // Perform the fetch.
    do {
        try controller.performFetch()
    } catch {
        fatalError("Unresolved error \(error)")
    }
    
    return controller
}()

移动和更新操作

然后我们执行以下操作

  • 要么将项目从正常部分移动到固定部分,要么将项目从固定部分移动到正常部分。
  • 更新内容。

func updatePinned(_ objectID: NSManagedObjectID, _ pinned: Bool) {
    let coreDataStack = CoreDataStack.INSTANCE
    let backgroundContext = coreDataStack.backgroundContext

    // TODO: Can we optimize the code, to avoid fetching the entire model object?
    backgroundContext.perform {
        let nsPlainNote = try! backgroundContext.existingObject(with: objectID) as! NSPlainNote
        // This will trigger "move". The cell shall move to different section.
        nsPlainNote.pinned = pinned
        
        // Can we trigger "update" as well?
        if nsPlainNote.pinned {
            nsPlainNote.body = nsPlainNote.title! + "(Pinned)"
        } else {
            nsPlainNote.body = nsPlainNote.title
        }
        
        RepositoryUtils.saveContextIfPossible(backgroundContext)
    }
}

NSFetchedResultsControllerDelegate

extension ViewController: NSFetchedResultsControllerDelegate {
    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
        
        if type == NSFetchedResultsChangeType.insert {
            print("Insert Object: \(newIndexPath)")
            
            blockOperations.append(
                BlockOperation(block: { [weak self] in
                    if let this = self {
                        this.collectionView!.insertItems(at: [newIndexPath!])
                    }
                })
            )
        }
        else if type == NSFetchedResultsChangeType.update {
            print("Update Object: \(indexPath)")
            blockOperations.append(
                BlockOperation(block: { [weak self] in
                    if let this = self {
                        this.collectionView!.reloadItems(at: [indexPath!])
                    }
                })
            )
        }
        else if type == NSFetchedResultsChangeType.move {
            print("Move Object: \(indexPath) to \(newIndexPath)")
            
            blockOperations.append(
                BlockOperation(block: { [weak self] in
                    if let this = self {
                        this.collectionView!.moveItem(at: indexPath!, to: newIndexPath!)
                    }
                })
            )
        }
        else if type == NSFetchedResultsChangeType.delete {
            print("Delete Object: \(indexPath)")
            
            blockOperations.append(
                BlockOperation(block: { [weak self] in
                    if let this = self {
                        this.collectionView!.deleteItems(at: [indexPath!])
                    }
                })
            )
        }
    }
    
    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
        if type == NSFetchedResultsChangeType.insert {
            print("Insert Section: \(sectionIndex)")
            
            blockOperations.append(
                BlockOperation(block: { [weak self] in
                    if let this = self {
                        this.collectionView!.insertSections(IndexSet(integer: sectionIndex))
                    }
                })
            )
        }
        else if type == NSFetchedResultsChangeType.update {
            print("Update Section: \(sectionIndex)")
            
            blockOperations.append(
                BlockOperation(block: { [weak self] in
                    if let this = self {
                        this.collectionView!.reloadSections(IndexSet(integer: sectionIndex))
                    }
                })
            )
        }
        else if type == NSFetchedResultsChangeType.delete {
            print("Delete Section: \(sectionIndex)")
            
            blockOperations.append(
                BlockOperation(block: { [weak self] in
                    if let this = self {
                        this.collectionView!.deleteSections(IndexSet(integer: sectionIndex))
                    }
                })
            )
        }
    }
    
    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        collectionView!.performBatchUpdates({ () -> Void in
            for operation: BlockOperation in self.blockOperations {
                operation.start()
            }
        }, completion: { (finished) -> Void in
            self.blockOperations.removeAll(keepingCapacity: false)
        })
    }
}

我们希望将项目从 Normal 部分移动并更新到 Pinned 部分。

我们执行

updatePinned(objectId, true)

只打印以下内容

Move Object: Optional([1, 12]) to Optional([0, 0])

我们预计除了NSFetchedResultsChangeType.moveNSFetchedResultsChangeType.update 也应该发生。但是,事实并非如此。只有NSFetchedResultsChangeType.move 发生。


解决方法(这是错误的方法!请勿应用!)

动画结束后我尝试reloadData

func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
    collectionView!.performBatchUpdates({ () -> Void in
        for operation: BlockOperation in self.blockOperations {
            operation.start()
        }
    }, completion: { (finished) -> Void in
        self.blockOperations.removeAll(keepingCapacity: false)
        
        // Do not do this! As, it will cause NSFetchedResultsController malfuntion after some time. 
        // You will soon realize NSFetchedResultsController is wrongly placing a pinned 
        // note in normal section.
        // Or even worst, it will issue didChange callback with wrong NSFetchedResultsChangeType value
        self.collectionView.reloadData()
    })
}

乍一看似乎一切正常。但是,如果您多次执行 pin 和 unpin 操作,您会注意到 NSFetchedResultsController 将注释放在错误的部分。它会将固定的笔记放在普通部分,将普通笔记放在固定部分。

或者更糟糕的是,它会发出带有错误NSFetchedResultsChangeType 值的didChange 回调


演示

以下是说明上述问题的演示代码。

https://github.com/yccheok/UICollectionView-02/tree/stackoverflow

如您所见,移动后,未执行更新。我们可以观察

  1. 未绘制蓝色图钉图标
  2. 橙色背景颜色正文未更新

只有在我们显式执行滚动时才会执行更新。


我可以知道,更改项目部分(以便有移动动画)和更新项目内容(以便调用cellForItemAt 函数)的正确方法是什么?

【问题讨论】:

    标签: ios swift core-data uicollectionview


    【解决方案1】:

    (在 r/iOSProgramming 交叉移植我的答案,以便 Stackoverflow 用户也能找到它) NSFRC 的移动总是意味着更新。对象“移动”的唯一原因是其排序键的值发生变化,这意味着该对象也已更新。

    编辑: 我明白了,我认为您在与 NSFRC 一起重新加载 UICollectionView 时遇到了常见的边缘情况之一。你不应该在这里使用reloadItems(),不管这有多不直观。相反,使用cellForRow(at:) 并创建一个更新该单元格数据的方法。 UICollectionView 和 UITableView 都是如此。

    检查其他人是如何做到的:

    【讨论】:

    • 即便如此,如果我们在收到 FRC 的“move”后发出this.collectionView!.moveItem(at: indexPath!, to: newIndexPath!),collectionView 的 cellForItemAt 将不会被调用。因为 collectionView 的 cellForItemAt 是唯一重绘单元 UI 的地方。那么,当 FRC 发生“移动”时,调用 collectionView 的 cellForItemAt 的好方法是什么?
    • 您好,感谢您的链接。看完代码后,我有两个问题。我可以知道,在收到NSFetchedResultsChangeType.update 时,是否有任何关于this.collectionView!.reloadItems(at: [indexPath!]) 的消息来源不应该使用?
    • 另一个问题是,在处理NSFetchedResultsChangeType.update 时,我注意到他们处理的是collectionView?.deleteItems(at: [old]),然后是collectionView?.insertItems(at: [new])。但是,如果我们这样做,我们将无法观察到将项目从一个位置移动到另一个位置的任何动画。
    • @CheokYanCheng 我不记得看过任何关于避免使用 NSFRC 的 reloadItems 的 Apple 文档。这是已经尝试过并经常使用 NSFRC 的人传授的知识(包括使用删除+插入代替移动)。如果有人说这已记录在案,请索取链接并将其发布在您的原始帖子中。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-04-09
    • 2022-06-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-13
    相关资源
    最近更新 更多