【发布时间】:2021-06-24 14:01:33
【问题描述】:
目前,我有一个UICollectionView,它由 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.move,NSFetchedResultsChangeType.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
如您所见,移动后,未执行更新。我们可以观察
- 未绘制蓝色图钉图标
- 橙色背景颜色正文未更新
只有在我们显式执行滚动时才会执行更新。
我可以知道,更改项目部分(以便有移动动画)和更新项目内容(以便调用cellForItemAt 函数)的正确方法是什么?
【问题讨论】:
标签: ios swift core-data uicollectionview