使用 NSFetchedResultsController 和核心数据移动时 UITableView 单元格消失

Posted

技术标签:

【中文标题】使用 NSFetchedResultsController 和核心数据移动时 UITableView 单元格消失【英文标题】:UITableView cells disappearing when moving using NSFetchedResultsController and Core Data 【发布时间】:2017-11-13 09:40:15 【问题描述】:

我正在构建我的第一个 Core Data 应用程序,并尝试实现用户可以通过按住和拖动来移动单元格的功能。

我遇到的问题是,当细胞被释放时,它们会消失。但是,如果您向下滚动足够多然后又弹出,它们会重新出现,尽管是按照原来的顺序,而不是重新排列的顺序。

A screen-recording of the bug can be found here

知道我哪里出错了吗?


我的 UITableViewController 子类:

import UIKit
import CoreData
import CoreGraphics

class MainTableViewController: UITableViewController 

    let context = AppDelegate.viewContext

    lazy var fetchedResultsController: TodoFetchedResultsController = 
        return TodoFetchedResultsController(managedObjectContext: self.context, tableView: self.tableView)
    ()

    override func viewDidLoad() 
        super.viewDidLoad()
        navigationItem.leftBarButtonItem = editButtonItem
        fetchedResultsController.tryFetch()

        let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(longPressGestureRecognized(gestureRecognizer:)))
    tableView.addGestureRecognizer(longPressRecognizer)


    var snapshot: UIView? = nil
    var path: IndexPath? = nil

    @objc
    func longPressGestureRecognized(gestureRecognizer: UILongPressGestureRecognizer) 
        let state = gestureRecognizer.state
        let locationInView = gestureRecognizer.location(in: tableView)
        let indexPath = tableView.indexPathForRow(at: locationInView)

        switch state 
        case .began:
            if indexPath != nil 
                self.path = indexPath
                let cell = tableView.cellForRow(at: indexPath!) as! TodoTableViewCell
                snapshot = snapshotOfCell(cell)
                var center = cell.center
                snapshot!.center = center
                snapshot!.alpha = 0.0
                tableView.addSubview(snapshot!)

                UIView.animate(withDuration: 0.1, animations:  () -> Void in
                    center.y = locationInView.y
                    self.snapshot!.center = center
                    self.snapshot!.transform = CGAffineTransform(scaleX: 1.05, y: 1.05)
                    self.snapshot!.alpha = 0.98
                    cell.alpha = 0.0
                , completion:  (finished) -> Void in
                    if finished 
                        cell.isHidden = true
                    
                )
            
        case .changed:
            if self.snapshot != nil 
                var center = snapshot!.center
                center.y = locationInView.y
                snapshot!.center = center
                if ((indexPath != nil) && (indexPath != self.path)) 
                    // Move cells
                    tableView.moveRow(at: self.path!, to: indexPath!)
                    self.path = indexPath
                
            
        default:
            if self.path != nil 
                let cell = tableView.cellForRow(at: self.path!) as! TodoTableViewCell
                cell.isHidden = false
                cell.alpha = 0.0
                UIView.animate(withDuration: 0.1, animations:  () -> Void in
                    self.snapshot!.center = cell.center
                    self.snapshot!.transform = CGAffineTransform.identity
                    self.snapshot!.alpha = 0.0
                    cell.alpha = 1.0
                , completion:  (finished) -> Void in
                    if finished 
                        cell.isHidden = true // FIXME: - Something up here?
                    
                )
            
        
    

    func snapshotOfCell(_ inputView: UIView) -> UIView 
    UIGraphicsBeginImageContextWithOptions(inputView.bounds.size, false, 0.0)

        guard let graphicsContext = UIGraphicsGetCurrentContext() else  fatalError() 
        inputView.layer.render(in: graphicsContext)

        et image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()

        let cellSnapshot: UIView = UIImageView(image: image)
        cellSnapshot.layer.masksToBounds = false
        cellSnapshot.layer.cornerRadius = 0.0
        cellSnapshot.layer.shadowOffset = CGSize(width: -5.0, height: 0.0)
        cellSnapshot.layer.shadowRadius = 5.0
        cellSnapshot.layer.shadowOpacity = 0.4
        return cellSnapshot
    

    // MARK: - Table view data source

    override func numberOfSections(in tableView: UITableView) -> Int 
        return fetchedResultsController.sections?.count ?? 0
    

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int 
        guard let section = fetchedResultsController.sections?[section] else  return 0 
        return section.numberOfObjects
    

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell 
        let cell = tableView.dequeueReusableCell(withIdentifier: "Todo Cell", for: indexPath) as! TodoTableViewCell

        return configureCell(cell, at: indexPath)
    

    private func configureCell(_ cell: TodoTableViewCell, at indexPath: IndexPath) -> TodoTableViewCell 
        let todo = fetchedResultsController.object(at: indexPath)

        do 
            try cell.update(with: todo)
         catch 
            print("\(error)")
        

        return cell
    

    func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool 
        return true
    

    // Support editing the table view.
    override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) 
        if editingStyle == .delete 
            let todoToDelete = fetchedResultsController.object(at: indexPath)
            context.delete(todoToDelete)
         else if editingStyle == .insert 
            // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
        
        (UIApplication.shared.delegate as! AppDelegate).saveContext()
        tableView.reloadData()
    

    // MARK: - Navigation
    ...

我的 NSFetchedResultsControllerSubclass:

class TodoFetchedResultsController: NSFetchedResultsController<Todo>, NSFetchedResultsControllerDelegate 
    private let tableView: UITableView

    init(managedObjectContext: NSManagedObjectContext, tableView: UITableView) 
        self.tableView = tableView

        let request: NSFetchRequest<Todo> = Todo.fetchRequest()
        let sortDescriptor = NSSortDescriptor(key: "dueDate", ascending: true)
        request.sortDescriptors = [sortDescriptor]
        super.init(fetchRequest: request, managedObjectContext: managedObjectContext, sectionNameKeyPath: nil, cacheName: nil)

        self.delegate = self

        tryFetch()
    

    func tryFetch() 
        do 
            try performFetch()
         catch 
            print("Unresolved error: \(error)")
        
    


    // MARK: - Fetched Results Controlle Delegate

    // Handle insertion, deletion, moving and updating of rows.
    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>,
                    didChange anObject: Any,
                    at indexPath: IndexPath?,
                    for type: NSFetchedResultsChangeType,
                    newIndexPath: IndexPath?) 
        switch type 
        case .insert:
            if let insertIndexPath = newIndexPath 
                self.tableView.insertRows(at: [insertIndexPath], with: .fade)
            
        case .delete:
            if let deleteIndexPath = indexPath 
                self.tableView.deleteRows(at: [deleteIndexPath], with: .fade)
            
        case .update:
            if let updateIndexPath = indexPath 
                if let cell = self.tableView.cellForRow(at: updateIndexPath) as! TodoTableViewCell? 
                    let todo = self.object(at: updateIndexPath)

                    do 
                        try cell.update(with: todo)
                     catch 
                        print("error updating cell: \(error)")
                    
                
            
        case .move:
            if let deleteIndexPath = indexPath 
                self.tableView.deleteRows(at: [deleteIndexPath], with: .fade)
            
            if let insertIndexPath = newIndexPath 
                self.tableView.insertRows(at: [insertIndexPath], with: .fade)
            
            break
        
    

    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) 
        tableView.reloadData()
    

【问题讨论】:

您应该创建另一个属性 (sortOrder),它是您的主要 sortDescriptor,并在用户完成时更改。在您的示例中,第一个 ToDo 的 sortOrder 应该分配给大于第三个的 sortOrder 的某个值。 @james:你找到解决方案了吗? 【参考方案1】:

您的代码会更新表格视图的 UI,但不会更新您的数据模型以获得新的顺序。下次您的表格视图需要显示一个单元格时,您的代码会为其提供与拖动之前相同的信息——例如,tableView(_:, cellForRowAt:) 仍然以旧顺序返回单元格,因为没有任何东西可以改变那里发生的事情。这不会发生,直到您滚开然后向后滚动,因为那是表格视图需要调用该方法的时候。旧的顺序会继续,只是因为你从不改变它,你只是做一个临时的 UI 更新。

如果您希望对表中的项目进行重新排序,则需要更新数据模型以包含该订单。可能这意味着在 Core Data 中添加一个新字段,一个称为sortOrder 之类的整数。然后更新sortOrder,使其与拖放中的新顺序相匹配。

【讨论】:

以上是关于使用 NSFetchedResultsController 和核心数据移动时 UITableView 单元格消失的主要内容,如果未能解决你的问题,请参考以下文章

核心数据保存竞争条件错误

在 Swift 3 中难以配置 NSFetchedResultsController

为啥 beginUpdates/endUpdates 会重置表视图位置以及如何阻止它这样做?

测试使用

第一篇 用于测试使用

在使用加载数据流步骤的猪中,使用(使用 PigStorage)和不使用它有啥区别?