简体   繁体   English

当涉及部分操作时,不会调用 performBatchUpdates 完成处理程序

[英]performBatchUpdates completion handler is not called when there is section operation involved

So far, here's are the code snippets that almost work for NSFetchedResultsController + UICollectionView , based on the information provided到目前为止,这里的是代码段,几乎工作NSFetchedResultsController + UICollectionView ,根据所提供的信息

Please note that, there are 2 [BlockOperation] , as reloadItems and moveItem doesn't play well within single performBatchUpdates .请注意,有 2 个[BlockOperation] ,因为reloadItemsmoveItem在单个performBatchUpdates中不能很好地发挥作用。 Based on the workaround proposed in the video, we have to call reloadItems in a separate performBatchUpdates .根据视频中提出的解决方法,我们必须在单独的performBatchUpdates调用reloadItems

We also do not follow 100% methods (Perform reloadItems typed performBatchUpdates first, followed by insert/ move/ delete typed performBatchUpdates) proposed in the video.我们也没有遵循视频中提出的 100% 方法(先执行reloadItems类型 performBatchUpdates,然后插入/移动/删除类型 performBatchUpdates)。

This is because we notice that it doesn't work well even for simple case.这是因为我们注意到即使对于简单的情况它也不能很好地工作。 Some strange behaviour including reloadItems will cause duplicated cell UI to be shown on screen .一些奇怪的行为包括reloadItems会导致重复的单元格 UI 显示在屏幕上 The "almost" work method we found are我们发现的“几乎”工作方法是

  • Perform performBatchUpdates for insert, move and delete为插入、移动和删除执行 performBatchUpdates
  • At completion handler of performBatchUpdates, perform another performBatchUpdates for reloadItems在 performBatchUpdates 的完成处理程序中,为 reloadItems 执行另一个 performBatchUpdates

NSFetchedResultsController + UICollectionView integration 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 }
        })
    }
}

The above way, works "almost" well when no "section" operations involved.当不涉及“部分”操作时,上述方式“几乎”工作得很好。

在此处输入图片说明

For the above animation, you will observe logging对于上面的动画,您将观察到日志记录

>> move
blockOperations completed
>> move
blockOperations completed
>> move
blockOperations completed

However, when a section is being added/ removed, the completion handler of performBatchUpdates is not being called!但是,当添加/删除部分时,不会调用performBatchUpdates的完成处理程序!

在此处输入图片说明

For the above animation, you will observe logging对于上面的动画,您将观察到日志记录

>> section delete
>> move
>> section insert
>> move

This means the completion handler block is not executed!这意味着未执行完成处理程序块! Does anyone know why it is so, and how I can workaround with this issue?有谁知道为什么会这样,我该如何解决这个问题?

I expect "blockOperations completed" should be printed out.我希望应该打印出“blockOperations completed”。 The expected log should be预期的日志应该是

>> section delete
>> move
blockOperations completed
>> section insert
>> move
blockOperations completed

Thanks.谢谢。

I tested this on Xcode 12 and Xcode 13.0 beta.我在 Xcode 12 和 Xcode 13.0 beta 上对此进行了测试。

On Xcode 12 I can reproduce the bug that you describe:在 Xcode 12 上,我可以重现您描述的错误:
When changing an object so that a whole section gets removed, the completion handler is not getting called.更改对象以删除整个部分时,不会调用完成处理程序。 When performing another subsequent change, I get two completion handler calls.在执行另一个后续更改时,我收到两个完成处理程序调用。

On Xcode 13 however, the issue is not reproducible in my tests.但是,在 Xcode 13 上,该问题在我的测试中无法重现。 I get proper callbacks when a section gets cleared and is removed.当一个部分被清除并被删除时,我会得到适当的回调。
Nevertheless I still get an odd message in the console saying尽管如此,我仍然在控制台中收到一条奇怪的消息说

[Snapshotting] Snapshotting a view (xxx, StackoverflowDemo.Cell) that has not been rendered at least once requires afterScreenUpdates:YES. [Snapshotting] 对一个至少没有渲染过一次的视图(xxx,StackoverflowDemo.Cell)进行快照需要afterScreenUpdates:YES。

My conclusion at this point is, that this is a bug within the system, that has been fixed in iOS 15.我此时的结论是,这是系统中的一个错误,已在 iOS 15 中修复。

[Update] [更新]

Regardless I have updated your code to achieve the proper behavior on both os versions.不管我已经更新了您的代码以在两个操作系统版本上实现正确的行为。

The key concepts are:关键概念是:

  • first execute single value updates首先执行单值更新
  • second execute section updates第二个执行部分更新
  • in case of a move also execute a reload in the completion block, otherwise possible simultaneous updates will not be rendered在移动的情况下也会在完成块中执行重新加载,否则将不会呈现可能的同时更新

It may be possible to refine the last step if you store moved indexPaths and only reload these rows.如果您存储移动的 indexPaths 并且仅重新加载这些行,则可以优化最后一步。

This is the code that I added to reproduce the issue.这是我为重现该问题而添加的代码。
I you want to test, please do the following steps:我要测试,请执行以下步骤:

  1. create a new Xcode project创建一个新的 Xcode 项目
  2. delete ViewController, SceneDelegate, Storyboard删除 ViewController、SceneDelegate、Storyboard
  3. remove Storyboard and Scene references from info.plist从 info.plist 中删除 Storyboard 和 Scene 引用
  4. replace the content of AppDelegate with the code below (just minimal boilerplate view/data setup plus delegate methods)用下面的代码替换 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()
            }
        })
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM