使用 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