使用 NSFetchedResultsController 核心数据重新排序 TableView 单元格 - Swift 3

Posted

技术标签:

【中文标题】使用 NSFetchedResultsController 核心数据重新排序 TableView 单元格 - Swift 3【英文标题】:Reorder TableView Cells With NSFetchedResultsController Core Data - Swift 3 【发布时间】:2017-02-25 22:45:36 【问题描述】:

我正在使用 NSFetchedResultsController。我找不到任何针对Swift 3 的简单教程。

这就是我到目前为止所做的。我已经使用从核心数据中获取插入数据的NSFetchedResultsController 成功填充了我的表。我在我的核心数据模型中创建了一个名为orderPosition 的属性,它被声明为Int32。关于在插入时将数据添加和保存到核心数据,我没有对这个属性做任何事情。

在我初始化NSFetchedResultsController 的提取func 中,我修改了排序描述符以包含以下代码:

let sortDescriptor = NSSortDescriptor(key: "orderPosition", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]

然后我实现了tableView func:

func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) 

    attemptFetch()
    var objects = self.controller.fetchedObjects! 
    self.controller.delegate = nil

    let object = objects[sourceIndexPath.row]
    objects.remove(at: sourceIndexPath.row)
    objects.insert(object, at: destinationIndexPath.row)

    var i = 0
    for object in objects 
        object.setValue(i += 1, forKey: "orderPosition")
    

    appdel.saveContext()
    self.controller.delegate = self


didChange anObject 函数的实现中,我包含了.insert.delete.update.move 的开关盒。 我的.move 案例如下所示:

case.move:
        if let indexPath = indexPath 
            tableView.deleteRows(at: [indexPath], with: .fade)
        
        if let indexPath = newIndexPath 
            tableView.insertRows(at: [indexPath], with: .fade)
        
        break

请帮我解决这个问题。我花了数周时间试图理解这一点。我已经经历了很多(如果不是全部的话)堆栈溢出问题。我从一个中实现了一些循环想法,因为我有一个类似的思考过程来解决这个问题,但它不起作用。我遇到的没有 Swift 3 核心数据教程/视频可以真正帮助我很好地解决和理解这个问题。我不想就此放弃。

【问题讨论】:

我收回了标志,不幸的是我没有在 Swift 中编码。然而,这可能是一个过时的教程,但也许你可以从中得到提示,除非除了我之前提供的重复链接之外,没有人为你提供解决方案。 iosnomad.com/blog/2014/8/6/… 谢谢,我去看看。这很令人沮丧,但我决心用这个来解决这个问题。 @JoséNeto 我想实现表格视图重新排序,我可以在其中单击编辑、移动单元格、单击完成并让它保存新订单并保留我的核心数据模型。任何帮助表示赞赏。 @Sneak 谢谢你的链接。我已经多次阅读这个“......诡计”教程,试图理解它。不幸的是,它确实很先进。 @Sneak 你和我有相似的学习方法。我总是试图打破一个项目以获得更深入的理解。别担心,我还没有放弃这样做。在我打字的时候,我把它打开了,我正在尝试过去哈哈再次感谢,我真的很感谢你能帮我解决这个问题。 【参考方案1】:

刚刚在我的项目中实现了类似的功能。结果比看起来更容易。

这是您的 moveRowAt 方法的更新

func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) 

    // NOT NEEDED, SINCE I ASSUME THAT YOU ALREADY FETCHED ON INITIAL LOAD
    // attemptFetch()

    var objects = self.controller.fetchedObjects! 

    // NOT NEEDED
    //self.controller.delegate = nil

    let object = objects[sourceIndexPath.row]
    objects.remove(at: sourceIndexPath.row)
    objects.insert(object, at: destinationIndexPath.row)

    // REWRITEN BELOW
    //var i = 0
    //for object in objects 
    //    object.setValue(i += 1, forKey: "orderPosition")
    //

    for (index, item) in items.enumerated() 
        item.orderPosition = index
    

    appdel.saveContext()
    //self.controller.delegate = self


didChange 中,从技术上讲,您不需要任何代码。当您在其中移动项目时,该表将就地更新。当您保存上下文时,您在 coreData 中的数据会更新。因此,下次获取数据时,根据您拥有的 sortDescriptor,它将按 orderPosition 排序。

为我工作。如果您有任何问题,请告诉我。

【讨论】:

【参考方案2】:

Swift 与 C 的不同之处在于 =、+= 和其他类似赋值的操作返回 Void。因此,您的行 object.setValue(i += 1, forKey: "orderPosition")i 设置为多一个,但始终将位置设置为 0。请尝试:

var i = 0
for object in objects 
    i += 1
    object.setValue(i, forKey: "orderPosition")

或者如果你想稍微简化你的代码,你可以这样做

for (i, object) in objects.enumerated() 
    object.setValue(i, forKey: "orderPosition")

【讨论】:

【参考方案3】:

四处走动:

    var objects = frc.fetchedObjects!

    // Disable fetchedresultscontroller updates.

    let obj objects.remove(at: indexPath.item)
    objects.insert(obj, at: newIndexPath)

    for (i, obj) in objects.enumerated() 
        obj.orderIndex = Int32(i)
    

    // Enable fetchedresultscontroller updates.

    context.saveContext()

TLDR:从 fetchedresultscontroller 禁用更新会导致其他事情在您身上崩溃,或者使用不会让您崩溃的解决方法。

我关于为什么要锁定的完整故事。

禁用的东西在那里,因为表格视图已经移动了单元格。如果您随后设置 orderIndex,它会尝试再次移动可能会像在我身上一样崩溃的东西。

我的解决方法是没有 .move 案例

    case .update, .move:
        print("update")
        self.reloadRows(at: [indexPath!], with: .automatic)

无论如何我都不需要,因为我现在只使用 tableview 移动。如果你想让 fetchedresultscontroller 也移动东西,你不必有这种情况 .update、.move 而是自己处理 .move

    case .move:
        print("move")
        self.moveRow(at: indexPath!, to: newIndexPath!)

但不要忘记在下面实现锁定或我的替代方案。

另一个在 endUpdates 上不会让我崩溃的解决方法是

    if let toIndexPath = newIndexPath 
        if type == .update && toIndexPath != indexPath!
            print("it should be a move.")
            print("move")
            self.moveRow(at: indexPath!, to: newIndexPath!)
            return
        
    

它检测 newIndexPath != indexPath(旧索引路径) 的更新,本质上它检测到未声明为移动的移动并为我解决了问题,但这对我来说太奇怪了。

以下是 fetchedresultscontrollers 对我的 orderIndex 更改的回答,这应该可以解释为什么您希望 fetchedresultscontroller 移动内容以更好地实现我的“伪装为更新的移动”检测或在更新期间禁用对表的更新对象,这很好,因为您的移动表格已经对单元格本身进行了重新排序。

    from: Optional([0, 2])
    to: Optional([0, 1])
    update
    from: Optional([0, 1])
    to: Optional([0, 0])
    update
    from: Optional([0, 0])
    to: Optional([0, 2])
    move
    ... Assertion failure in ...
    attempt to perform an insert and a move to the same index path (<NSIndexPath: 0xc000000000400016> length = 2, path = 0 - 2)
    (null)

编辑:

我认为“伪装成更新的移动”检测不会产生废话,因为它只是重新加载所有单元格。因为然后它通过tableview本身移动了tableviewcell 1次+通过检测移动了3次(无用)。

为什么 fetchedresultscontroller 使用这种调用模式?它可能会疯狂地尝试按顺序获取表格,就好像更新来自外部并且表格需要更新一样。但是它不知道表格单元格已经被表格视图按正确的顺序排列了,所以它在 super.endUpdates() 上由于某种原因而严重崩溃。

SOO:可能最简单的解决方案是禁用对 tableview 的更新。如何?现在不知道,我要去睡觉了。

【讨论】:

我将 frc.delegate 设置为 nil 以“禁用 fetchedresultscontroller 更新”,然后将其设置为 self 以在 tableView:moveRowAt 方法中重新启用它们并且它可以工作。好建议!【参考方案4】:

假设核心数据和 fetchedResultsController:关于 moveRowAt,从和到索引路径行在该部分内。 如果您在各个部分之间移动,则需要计算出要在阵列中移动的实际行。即:如果您将第三部分的第二行移动到第一部分的第二行,您实际上需要知道所有部分中有多少行才能删除正确的行并插入到正确的位置。

我使用以下方法来获取正确的行:-

    // Do not trigger delegate methods when changes are made to core data by the user
        fetchedResultsController.delegate = nil
        var fromIndex = fromIndexPath.row
        var toIndex = toIndexPath.row
        //      print ("from row ",fromIndexPath.row)
//work out correct row to remove at based on it's current row and section
        for sectionIndex in 0..<fromIndexPath.section
        
            fromIndex += fetchedResultsController.sections![sectionIndex].numberOfObjects
        
            //print ("fromIndex ",fromIndex)
// remove the row at it's old position
       toDoData.remove(at: fromIndex)
//work out the correct row to insert at based on which section it's going to & row in that section
        for sectionIndex in 0..<toIndexPath.section
        
            toIndex += fetchedResultsController.sections![sectionIndex].numberOfObjects
            //print ("toIndex ",toIndex)
            if sectionIndex == fromIndexPath.section
            
                toIndex -= 1 // Remember, controller still thinks this item is in the old section
                //print ("-= toIndex",toIndex)
            
        
// put the item back into he array at new position
         toDoData.insert(item, at: toIndex)

注意:toDoData 是来自 fetchedResultsController 的记录数组

【讨论】:

以上是关于使用 NSFetchedResultsController 核心数据重新排序 TableView 单元格 - Swift 3的主要内容,如果未能解决你的问题,请参考以下文章

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

在 Swift 3 中难以配置 NSFetchedResultsController

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

测试使用

第一篇 用于测试使用

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