使用NSFetchedResultsController和Core Data移动时UITableView单元格消失

时间:2022-09-18 08:24:23

I'm building my first Core Data app and am trying to implement a feature where users can move cells by holding and dragging.

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

The problem I'm getting is that when the cells are released they disappear. However, if you scroll down enough and then pop back up, they reappear, albeit in their original order, not the rearranged order.

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

A screen-recording of the bug can be found here

可以在此处找到错误的屏幕录制

Any idea where I'm going wrong?

知道我哪里错了吗?


My UITableViewController Sub-Class:

我的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
    ...
}

My 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()
    }
}

1 个解决方案

#1


0  

Your code updates the table view's UI, but it does not update your data model to have the new order. The next time your table view needs to display a cell, your code gives it the same information as before the drag-- For example, tableView(_:, cellForRowAt:) still returns cells in the old order, because there's nothing that changes what happens there. This doesn't happen until you scroll away and then scroll back because that's when the table view needs to call that method. The old order will continue, simply because you never change it, you only make a temporary UI update.

您的代码更新了表视图的UI,但它不会更新您的数据模型以获得新订单。下次表视图需要显示一个单元格时,代码会给它提供与拖动之前相同的信息 - 例如,tableView(_:,cellForRowAt :)仍然按旧顺序返回单元格,因为没有什么可以改变什么发生在那里。直到您滚动然后向后滚动才会发生这种情况,因为当表视图需要调用该方法时。旧订单将继续,只是因为您永远不会更改它,您只进行临时UI更新。

If you want to make it possible to re-order items in the table, you need to update your data model to include that order. Probably that means adding a new field in Core Data, an integer called something like sortOrder. Then update the sortOrder so that it matches the new order from the drag and drop.

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

#1


0  

Your code updates the table view's UI, but it does not update your data model to have the new order. The next time your table view needs to display a cell, your code gives it the same information as before the drag-- For example, tableView(_:, cellForRowAt:) still returns cells in the old order, because there's nothing that changes what happens there. This doesn't happen until you scroll away and then scroll back because that's when the table view needs to call that method. The old order will continue, simply because you never change it, you only make a temporary UI update.

您的代码更新了表视图的UI,但它不会更新您的数据模型以获得新订单。下次表视图需要显示一个单元格时,代码会给它提供与拖动之前相同的信息 - 例如,tableView(_:,cellForRowAt :)仍然按旧顺序返回单元格,因为没有什么可以改变什么发生在那里。直到您滚动然后向后滚动才会发生这种情况,因为当表视图需要调用该方法时。旧订单将继续,只是因为您永远不会更改它,您只进行临时UI更新。

If you want to make it possible to re-order items in the table, you need to update your data model to include that order. Probably that means adding a new field in Core Data, an integer called something like sortOrder. Then update the sortOrder so that it matches the new order from the drag and drop.

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