简体   繁体   中英

UITableView cells disappearing when moving using NSFetchedResultsController and Core Data

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

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:

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

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.

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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