SwiftUI重新排序列表中的CoreData对象

时间:2020-01-14 21:44:43

标签: core-data swiftui

我想更改从核心数据中检索对象的列表中行的顺序。移动行是可行的,但是问题是我无法保存更改。我不知道如何保存CoreData对象的已更改索引。

这是我的代码:

核心数据类:

public class CoreItem: NSManagedObject, Identifiable{
    @NSManaged public var name: String

}

extension CoreItem{
    static func getAllCoreItems() -> NSFetchRequest <CoreItem> {
        let request: NSFetchRequest<CoreItem> = CoreItem.fetchRequest() as! NSFetchRequest<CoreItem>
        let sortDescriptor = NSSortDescriptor(key: "date", ascending: true)
        request.sortDescriptors = [sortDescriptor]
        return request
    }
}

extension Collection where Element == CoreItem, Index == Int {
    func move(set: IndexSet, to: Int,  from managedObjectContext: NSManagedObjectContext) {

        do {
            try managedObjectContext.save()
        } catch {
            let nserror = error as NSError
            fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
        }
    }
} 

列表:



struct CoreItemList: View {

    @Environment(\.managedObjectContext) var managedObjectContext

    @FetchRequest(fetchRequest: CoreItem.getAllCoreItems()) var CoreItems: FetchedResults<CoreItem>



var body: some View {
      NavigationView{
          List {
            ForEach(CoreItems, id: \.self){
                   coreItem in
                    CoreItemRow(coreItem: coreItem)
                  }.onDelete {
                  IndexSet in let deleteItem = self.CoreItems[IndexSet.first!]
                  self.managedObjectContext.delete(deleteItem)

                  do {
                      try self.managedObjectContext.save()
                  } catch {
                      print(error)
                     }
                  }
                .onMove {
                    self.CoreItems.move(set: $0, to: $1, from: self.managedObjectContext)
              }
            }
             .navigationBarItems(trailing: EditButton())
           }.navigationViewStyle(StackNavigationViewStyle())
        }
    }

谢谢您的帮助。

3 个答案:

答案 0 :(得分:9)

注意:尽管我在示例项目中使用了并行逻辑,但该项目似乎正在工作,但以下答案未经测试。

答案有两部分。如Joakim Danielson所说,为了保留用户的首选订单,您需要将订单保存在CoreItem类中。修改后的类如下:

public class CoreItem: NSManagedObject, Identifiable{
    @NSManaged public var name: String
    @NSManaged public var userOrder: Int16
}

第二部分是根据userOrder属性对项目进行排序。初始化时,userOrder通常默认为零,因此在name中按userOrder进行排序可能很有用。假设您要执行此操作,然后在CoreItemList代码中:

@FetchRequest( entity: CoreItem.entity(),
                   sortDescriptors:
                   [
                       NSSortDescriptor(
                           keyPath: \CoreItem.userOrder,
                           ascending: true),
                       NSSortDescriptor(
                           keyPath:\CoreItem.name,
                           ascending: true )
                   ]
    ) var coreItems: FetchedResults<CoreItem>

第三部分是您需要告诉swiftui允许用户修改列表的顺序。如您的示例所示,这是通过onMove修饰符完成的。在该修饰符中,您可以执行按用户偏好的顺序对列表重新排序所需的操作。例如,您可以调用一个名为move的便捷函数,这样修饰符将显示为:

.onMove( perform: move )

您的move函数将被传递一个IndexSet和一个Int。索引集包含FetchRequestResult中所有要移动的项(通常只是一项)。 Int指示应将其移动到的位置。逻辑将是:

private func move( from source: IndexSet, to destination: Int) 
{
    // Make an array of items from fetched results
    var revisedItems: [ CoreItem ] = coreItems.map{ $0 }

    // change the order of the items in the array
    revisedItems.move(fromOffsets: source, toOffset: destination )

    // update the userOrder attribute in revisedItems to 
    // persist the new order. This is done in reverse order 
    // to minimize changes to the indices.
    for reverseIndex in stride( from: revisedItems.count - 1,
                                through: 0,
                                by: -1 )
    {
        revisedItems[ reverseIndex ].userOrder =
            Int16( reverseIndex )
    }
}

技术提醒:存储在modifiedItems中的项目是类(即通过引用),因此更新这些项目将必然会更新所获取结果中的项目。 @FetchedResults包装器将使您的用户界面反映新的顺序。

诚然,我是SwiftUI的新手。可能会有一个更优雅的解决方案!

Paul Hudson(Hacking With Swift)有很多细节。这是link,用于获取有关在列表中移动数据的信息。这是link,用于在SwiftUI中使用核心数据(它涉及删除列表中的项目,但与onMove逻辑非常相似)

答案 1 :(得分:1)

您可以在下面找到解决此问题的更通用的方法。与接受的答案相反,该算法最大限度地减少了需要更新的 CoreData 实体的数量。我的解决方案受到以下文章的启发:https://www.appsdissected.com/order-core-data-entities-maximum-speed/

首先,我声明一个 protocol 如下以用于您的模型 struct(或 class):

protocol Sortable {
    var sortOrder: Int { get set }
}

举个例子,假设我们有一个 SortItem 模型,它实现了我们的 Sortable 协议,定义为:

struct SortItem: Identifiable, Sortable {
    var id = UUID()
    var title = ""
    var sortOrder = 0
}

我们还有一个简单的 SwiftUI View,其相关 ViewModel 定义为(精简版):

struct ItemsView: View {
    @ObservedObject private(set) var viewModel: ViewModel
    
    var body: some View {
        NavigationView {
            List {
                ForEach(viewModel.items) { item in
                    Text(item.title)
                }
                .onMove(perform: viewModel.move(from:to:))
            }
        }
        .navigationBarItems(trailing: EditButton())
    }
}

extension ItemsView {
    class ViewModel: ObservableObject {
        @Published var items = [SortItem]()
        
        func move(from source: IndexSet, to destination: Int) {
            items.move(fromOffsets: source, toOffset: destination)

            // Note: Code that updates CoreData goes here, see below
        }
    }
}

在我继续算法之前,我想注意 destination 函数中的 move 变量在将项目向下移动时不包含新索引。假设只移动了一个item,检索新索引(移动完成后)可以实现如下:

func move(from source: IndexSet, to destination: Int) {
    items.move(fromOffsets: source, toOffset: destination)
    
    if let oldIndex = source.first, oldIndex != destination {
        let newIndex = oldIndex < destination ? destination - 1 : destination
        
        // Note: Code that updates CoreData goes here, see below
    }
}

对于 extension 属于 Array 类型的情况,算法本身实现为 ElementSortable。它由一个递归 updateSortOrder 函数和一个 private 辅助函数 enclosingIndices 组成,它检索包含在数组特定索引周围的索引,同时保留在数组边界内。完整的算法如下(解释如下):

extension Array where Element: Sortable {
    func updateSortOrder(around index: Int, for keyPath: WritableKeyPath<Element, Int> = \.sortOrder, spacing: Int = 32, offset: Int = 1, _ operation: @escaping (Int, Int) -> Void) {
        if let enclosingIndices = enclosingIndices(around: index, offset: offset) {
            if let leftIndex = enclosingIndices.first(where: { $0 != index }),
               let rightIndex = enclosingIndices.last(where: { $0 != index }) {
                let left = self[leftIndex][keyPath: keyPath]
                let right = self[rightIndex][keyPath: keyPath]
                
                if left != right && (right - left) % (offset * 2) == 0 {
                    let spacing = (right - left) / (offset * 2)
                    var sortOrder = left
                    for index in enclosingIndices.indices {
                        if self[index][keyPath: keyPath] != sortOrder {
                            operation(index, sortOrder)
                        }
                        sortOrder += spacing
                    }
                } else {
                    updateSortOrder(around: index, for: keyPath, spacing: spacing, offset: offset + 1, operation)
                }
            }
        } else {
            for index in self.indices {
                let sortOrder = index * spacing
                if self[index][keyPath: keyPath] != sortOrder {
                    operation(index, sortOrder)
                }
            }
        }
    }
    
    private func enclosingIndices(around index: Int, offset: Int) -> Range<Int>? {
        guard self.count - 1 >= offset * 2 else { return nil }
        var leftIndex = index - offset
        var rightIndex = index + offset
        
        while leftIndex < startIndex {
            leftIndex += 1
            rightIndex += 1
        }
        while rightIndex > endIndex - 1 {
            leftIndex -= 1
            rightIndex -= 1
        }
        
        return Range(leftIndex...rightIndex)
    }
}

首先,enclosingIndices 函数。它返回一个可选的 Range<Int>offset 参数定义 index 参数左侧和右侧封闭索引的距离。 guard 确保完整的封闭索引包含在数组中。此外,如果 offset 超出数组的 startIndexendIndex,封闭索引将分别向右或向左移动。因此,在数组的边界处,index 不一定位于封闭索引的中间。

其次,updateSortOrder 函数。它至少需要 index 开始更新排序顺序。这是 moveViewModel 函数的新索引。此外,updateSortOrder 期望一个 @escaping 闭包提供两个整数,这将在下面解释。所有其他参数都是可选的。 keyPath 默认为 \.sortOrder,符合 protocol 的期望。但是,如果用于排序的模型参数不同,则可以指定它。 spacing 参数定义了通常使用的排序顺序间距。这个值越大,除了移动的项目之外,不需要任何其他 CoreData 更新就可以执行更多的排序操作。 offset 参数不应真正被触及,而是用于函数的递归。

函数首先请求 enclosingIndices。如果这些被找到,当数组小于三个项时立即发生,或者在 updateSortOrder 函数的递归之一中,当 offset 是这样的它会超出数组的边界;那么在 else 的情况下,数组中所有项目的排序顺序将被重置。在这种情况下,如果 sortOrder 与 items 现有值不同,则调用 @escaping 闭包。它的实现将在下面进一步讨论。

当找到 enclosingIndices 时,包围索引的左右索引不是是移动项的索引被确定。已知这些索引后,这些索引的现有“排序顺序”值可通过 keyPath 获得。然后验证这些值是否不相等(如果在数组中以相同的排序顺序添加项目,则可能会发生这种情况)以及排序顺序与封闭索引数量减去移动项目之间的差异的除法将导致非整数值。这基本上会检查在最小间距 1 内是否还有一个位置可用于移动项目的潜在新排序顺序值。如果不是,则应将封闭索引扩展到下一个更高的{ {1}} 和算法再次运行,因此在这种情况下递归调用 offset

当一切都成功后,应该为封闭索引之间的项目确定新的间距。然后循环遍历所有封闭索引,并将每个项目的排序顺序与潜在的新排序顺序进行比较。如果更改,则调用 updateSortOrder 闭包。对于循环中的下一项,排序顺序值将再次更新。

此算法导致对 @escaping 闭包的回调最少。因为这仅在确实需要更新项目的排序顺序时发生。

最后,正如您可能猜到的,CoreData 的实际回调将在闭包中处理。定义算法后,@escaping ViewModel 函数将更新如下:

move

如果您对此方法有任何疑问,请告诉我。希望你喜欢。

答案 2 :(得分:0)

Int16出现问题,并将其更改为@NSManaged public var userOrder: NSNumber?,并在函数 NSNumber(value: Int16( reverseIndex ))

中解决了该问题

同样,我需要在函数中添加try? managedObjectContext.save()才能真正保存新订单。

现在工作正常-谢谢!