ios - 当涉及部分操作时,不调用 performBatchUpdates 完成处理程序
问题描述
到目前为止,根据提供的信息,这是几乎适用于NSFetchedResultsController
+的代码片段UICollectionView
- https://developer.apple.com/videos/play/wwdc2018/225(36:50开始)
- 如何在 UICollectionView performBatchUpdates 块中对移动、插入、删除和更新进行排序?
请注意,有 2 个[BlockOperation]
,因为在 single 中reloadItems
并moveItem
不能很好地发挥作用performBatchUpdates
。根据视频中提出的解决方法,我们必须调用reloadItems
单独的performBatchUpdates
.
我们也没有遵循reloadItems
视频中提出的 100% 方法(首先执行类型化 performBatchUpdates,然后是插入/移动/删除类型化 performBatchUpdates)。
这是因为我们注意到即使对于简单的情况它也不能很好地工作。一些奇怪的行为包括reloadItems
会导致重复的单元格 UI 显示在屏幕上。我们发现的“几乎”工作方法是
- 为插入、移动和删除执行 performBatchUpdates
- 在 performBatchUpdates 的完成处理程序中,为 reloadItems 执行另一个 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 completed”。预期的日志应该是
>> section delete
>> move
blockOperations completed
>> section insert
>> move
blockOperations completed
谢谢。
解决方案
我在 Xcode 12 和 Xcode 13.0 beta 上对此进行了测试。
在 Xcode 12 上,我可以重现您描述的错误:
更改对象以删除整个部分时,不会调用完成处理程序。当执行另一个后续更改时,我得到两个完成处理程序调用。
然而,在 Xcode 13 上,这个问题在我的测试中无法重现。当一个部分被清除并被删除时,我会得到适当的回调。
尽管如此,我仍然在控制台中收到一条奇怪的消息,说
[Snapshotting] 对至少未渲染一次的视图(xxx,StackoverflowDemo.Cell)进行快照需要 afterScreenUpdates:YES。
在这一点上,我的结论是,这是系统中的一个错误,已在 iOS 15 中修复。
[更新]
无论如何,我已经更新了您的代码以在两个操作系统版本上实现正确的行为。
关键概念是:
- 首先执行单值更新
- 第二个执行部分更新
- 在移动的情况下也在完成块中执行重新加载,否则可能的同时更新将不会被渲染
如果您存储移动的 indexPaths 并仅重新加载这些行,则可以优化最后一步。
这是我添加的重现问题的代码。
我要测试,请执行以下步骤:
- 创建一个新的 Xcode 项目
- 删除 ViewController、SceneDelegate、Storyboard
- 从 info.plist 中删除 Storyboard 和 Scene 引用
- 用下面的代码替换 AppDelegate 的内容(只是最小的样板视图/数据设置加上委托方法)
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()
}
})
}
}
推荐阅读
- r - 根据向量中的日期过滤 XTS 表
- reactjs - 停止在以前的日期删除事件 fullcalendar react
- android - AsyncTask 更新在 android 的其他类中调用的 ui 计数器
- tensorflow.js - Tensorflow.js 中有没有类似于 Python/Keras 的 to_categorical() 的实用函数?
- if-statement - 其他人真的是代码味道吗?编写 Go 条件的惯用方式
- node.js - 错误:EMFILE:打开的文件太多,打开“/app/build/index.html”
- python - 将 keras .h5 模型更改为 tensorflow .pb 模型时出现问题
- r - 在 Windows 10 中使用 Anaconda Navigator 安装 RStudio
- java - 向 ArrayList 添加多个对象打印效果不佳
- azure-webjobs - 带有 TimerTrigger 的 Azure WebJob 仅触发 5 次