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.