到目前为止,基于所提供的信息,以下是几乎适用于NSFetchedResultsController
+ UICollectionView
的代码片段
开始)
请注意,有两个[BlockOperation]
,因为reloadItems
和moveItem
在单个performBatchUpdates
中发挥得不太好。根据视频中提出的解决方案,我们必须在一个单独的reloadItems
中调用performBatchUpdates
。
我们也不遵循视频中提出的100%方法(首先执行reloadItems
类型化performBatchUpdates,然后是插入/移动/删除类型化performBatchUpdates)。
这是因为我们注意到,即使在简单的情况下,它也不能很好地工作。一些奇怪的行为(包括reloadItems
)将导致复制的单元格UI显示在屏幕上。我们发现的“几乎”工作方法是
执行另一个performBatchUpdates
NSFetchedResultsController + UICollectionView集成
private var blockOperations: [BlockOperation] = []
// reloadItems and moveItem do not play well together. We are using the following workaround proposed at
// https://developer.apple.com/videos/play/wwdc2018/225/
private var blockUpdateOperations: [BlockOperation] = []
extension DashboardViewController: NSFetchedResultsControllerDelegate {
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
if type == NSFetchedResultsChangeType.insert {
print(">> insert")
blockOperations.append(
BlockOperation(block: { [weak self] in
if let self = self {
self.collectionView!.insertItems(at: [newIndexPath!])
}
})
)
}
else if type == NSFetchedResultsChangeType.update {
print(">> update")
blockUpdateOperations.append(
BlockOperation(block: { [weak self] in
if let self = self, let indexPath = indexPath {
self.collectionView.reloadItems(at: [indexPath])
}
})
)
}
else if type == NSFetchedResultsChangeType.move {
print(">> move")
blockOperations.append(
BlockOperation(block: { [weak self] in
if let self = self, let newIndexPath = newIndexPath, let indexPath = indexPath {
self.collectionView.moveItem(at: indexPath, to: newIndexPath)
}
})
)
}
else if type == NSFetchedResultsChangeType.delete {
print(">> delete")
blockOperations.append(
BlockOperation(block: { [weak self] in
if let self = self {
self.collectionView!.deleteItems(at: [indexPath!])
}
})
)
}
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
if type == NSFetchedResultsChangeType.insert {
print(">> section insert")
blockOperations.append(
BlockOperation(block: { [weak self] in
if let self = self {
self.collectionView!.insertSections(IndexSet(integer: sectionIndex))
}
})
)
}
else if type == NSFetchedResultsChangeType.update {
print(">> section update")
blockOperations.append(
BlockOperation(block: { [weak self] in
if let self = self {
self.collectionView!.reloadSections(IndexSet(integer: sectionIndex))
}
})
)
}
else if type == NSFetchedResultsChangeType.delete {
print(">> section delete")
blockOperations.append(
BlockOperation(block: { [weak self] in
if let self = self {
self.collectionView!.deleteSections(IndexSet(integer: sectionIndex))
}
})
)
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
if blockOperations.isEmpty {
performBatchUpdatesForUpdateOperations()
} else {
collectionView.performBatchUpdates({ [weak self] () -> Void in
guard let self = self else { return }
for operation: BlockOperation in self.blockOperations {
operation.start()
}
self.blockOperations.removeAll(keepingCapacity: false)
}, completion: { [weak self] (finished) -> Void in
print("blockOperations completed")
guard let self = self else { return }
self.performBatchUpdatesForUpdateOperations()
})
}
}
private func performBatchUpdatesForUpdateOperations() {
if blockUpdateOperations.isEmpty {
return
}
collectionView.performBatchUpdates({ [weak self] () -> Void in
guard let self = self else { return }
for operation: BlockOperation in self.blockUpdateOperations {
operation.start()
}
self.blockUpdateOperations.removeAll(keepingCapacity: false)
}, completion: { [weak self] (finished) -> Void in
print("blockUpdateOperations completed")
guard let self = self else { return }
})
}
}
以上方式,工作“几乎”好时,不涉及“分段”操作。
对于上面的动画,您将看到日志记录。
>> move
blockOperations completed
>> move
blockOperations completed
>> move
blockOperations completed
但是,在添加/删除节时,不调用performBatchUpdates
的完成处理程序!。
对于上面的动画,您将看到日志记录。
>> section delete
>> move
>> section insert
>> move
这意味着未执行完成处理程序块!有人知道为什么会这样吗?我怎样才能解决这个问题?
我希望"blockOperations完成“应该打印出来。预期的日志应该是
>> section delete
>> move
blockOperations completed
>> section insert
>> move
blockOperations completed
谢谢。
发布于 2021-07-23 00:25:25
我在Xcode 12和Xcode 13.0beta上测试了这一点。
在Xcode 12上,我可以重现您描述的bug:
当更改对象以删除整个部分时,不调用完成处理程序。在执行另一个后续更改时,我会得到两个完成处理程序调用。
然而,在Xcode 13上,这个问题在我的测试中是不可重现的。当一个区段被清除并被移除时,我会得到正确的回调。
尽管如此,我还是在控制台上收到一条奇怪的信息:
快照快照一个视图(xxx,StackoverflowDemo.Cell),该视图至少有一次未呈现,需要“后屏幕更新”:是的。
我现在的结论是,这是系统中的一个bug,已经在iOS 15中修复了。
更新
--不管我更新了您的代码,都是为了在这两个os版本上实现正确的行为.
主要的概念是:
。
如果您存储已移动的indexPaths并仅重新加载这些行,则可能会细化最后一步。
这是我为再现问题而添加的代码。
我想测试一下,请执行以下步骤:
project
import UIKit
import CoreData
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
self.window = UIWindow(frame: UIScreen.main.bounds)
let layout = UICollectionViewFlowLayout()
layout.headerReferenceSize = CGSize(width: 30,height: 30)
layout.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
self.window?.rootViewController = UINavigationController.init(rootViewController: DashboardViewController(collectionViewLayout: layout) )
self.window?.makeKeyAndVisible()
return true
}
}
class DashboardViewController: UICollectionViewController {
let persistentContainer = PersistentContainer()
lazy var resultsController: NSFetchedResultsController<Entity>? = {
let fetchRequest = NSFetchRequest<Entity>(entityName: "Entity")
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "section", ascending: true), NSSortDescriptor(key: "name", ascending: false)]
let resultsController = NSFetchedResultsController(fetchRequest: fetchRequest,
managedObjectContext: self.persistentContainer.viewContext,
sectionNameKeyPath: "section",
cacheName: nil)
resultsController.delegate = self
try! resultsController.performFetch()
return resultsController
}()
private var itemOperations = [() -> Void]()
private var sectionOperations = [() -> Void]()
private var reloadRequired = false
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(add))
self.collectionView.register(Cell.self, forCellWithReuseIdentifier: "Cell")
self.collectionView.register(Header.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "Header")
}
var itemIndex: Int = 0
var section: Double = 0
@objc func add() {
let entity = Entity(context: self.persistentContainer.viewContext)
entity.name = Int64(self.itemIndex)
itemIndex += 1
entity.section = Int64(floor(self.section))
section += 0.5
try! self.persistentContainer.viewContext.save()
}
override func numberOfSections(in collectionView: UICollectionView) -> Int { return resultsController!.sections?.count ?? 0 }
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return self.resultsController!.sections![section].numberOfObjects }
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Header", for: indexPath) as! Header
let sectionInfo = self.resultsController!.sections?[indexPath.section]
header.label.text = sectionInfo?.name
return header
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let item = self.resultsController?.object(at: indexPath)
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! Cell
cell.label.text = String(describing: item?.name ?? -1)
return cell
}
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let item = self.resultsController?.object(at: indexPath)
item?.section = max(0, (item?.section ?? 0) - 1)
item?.name = 10 + (item?.name ?? 0)
}
}
@objc(Entity)
public class Entity: NSManagedObject {
@NSManaged public var name: Int64
@NSManaged public var section: Int64
}
class Cell: UICollectionViewCell {
let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = .lightGray
self.label.textAlignment = .center
self.label.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.label.frame = self.contentView.bounds
self.label.translatesAutoresizingMaskIntoConstraints = true
self.contentView.addSubview(self.label)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
}
class Header: UICollectionReusableView {
let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = .gray
self.label.textAlignment = .center
self.label.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.label.frame = self.bounds
self.label.translatesAutoresizingMaskIntoConstraints = true
self.addSubview(self.label)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
}
class PersistentContainer: NSPersistentContainer {
convenience init() {
// create object model
let nameProperty = NSAttributeDescription()
nameProperty.name = "name"
nameProperty.attributeType = .integer64AttributeType
let sectionProperty = NSAttributeDescription()
sectionProperty.name = "section"
sectionProperty.attributeType = .integer64AttributeType
let entity = NSEntityDescription()
entity.name = "Entity"
entity.managedObjectClassName = "Entity"
entity.properties = [nameProperty, sectionProperty]
let model = NSManagedObjectModel()
model.entities.append(entity)
// create container
self.init(name: "Foo", managedObjectModel: model)
let description = NSPersistentStoreDescription()
description.type = NSInMemoryStoreType
self.persistentStoreDescriptions = [description]
self.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
}
}
extension DashboardViewController: NSFetchedResultsControllerDelegate {
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
reloadRequired = false
if type == NSFetchedResultsChangeType.insert {
print(">> insert")
itemOperations.append { [weak self] in
if let self = self {
self.collectionView!.insertItems(at: [newIndexPath!])
}
}
}
else if type == NSFetchedResultsChangeType.update {
print(">> update")
itemOperations.append { [weak self] in
if let self = self, let indexPath = indexPath {
self.collectionView.reloadItems(at: [indexPath])
}
}
}
else if type == NSFetchedResultsChangeType.move {
print(">> move")
self.reloadRequired = true
itemOperations.append { [weak self] in
if let self = self, let newIndexPath = newIndexPath, let indexPath = indexPath {
self.collectionView.moveItem(at: indexPath, to: newIndexPath)
}
}
}
else if type == NSFetchedResultsChangeType.delete {
print(">> delete")
itemOperations.append { [weak self] in
if let self = self {
self.collectionView!.deleteItems(at: [indexPath!])
}
}
}
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
if type == NSFetchedResultsChangeType.insert {
print(">> section insert")
sectionOperations.append { [weak self] in
if let self = self {
self.collectionView!.insertSections(IndexSet(integer: sectionIndex))
}
}
}
else if type == NSFetchedResultsChangeType.update {
print(">> section update")
sectionOperations.append { [weak self] in
if let self = self {
self.collectionView!.reloadSections(IndexSet(integer: sectionIndex))
}
}
}
else if type == NSFetchedResultsChangeType.delete {
print(">> section delete")
sectionOperations.append { [weak self] in
if let self = self {
self.collectionView!.deleteSections(IndexSet(integer: sectionIndex))
}
}
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
collectionView.performBatchUpdates({ [weak self] () -> Void in
guard let self = self else { return }
// execute single item operations first
self.itemOperations.forEach { $0() }
// execute section operations afterwards
self.sectionOperations.forEach { $0() }
self.itemOperations.removeAll(keepingCapacity: false)
self.sectionOperations.removeAll(keepingCapacity: false)
}, completion: { [weak self] (finished) -> Void in
print("blockOperations completed")
guard let self = self else { return }
// in case of a move do a reload in case the item has also changed
// it will not update otherwise
if self.reloadRequired {
self.collectionView.reloadData()
}
})
}
}
https://stackoverflow.com/questions/68413796
复制相似问题