Search code examples
core-dataswiftui

SwiftUI reorder CoreData Objects in List


I want to change the order of the rows in a list that retrieves objects from the core data. Moving rows works, but the problem is that I can't save the changes. I don't know how to save the changed Index of the CoreData Object.

Here is my Code:

Core Data Class:

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

List:



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

Thank you for help.


Solution

  • Caveat: the answer below is untested, although I used parallel logic in a sample project and that project seems to be working.

    There's a couple parts to the answer. As Joakim Danielson says, in order to persist the user's preferred order you will need to save the order in your CoreItem class. The revised class would look like:

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

    The second part is to keep the items sorted based on the userOrder attribute. On initialization the userOrder would typically default to zero so it might be useful to also sort by name within userOrder. Assuming you want to do this, then in CoreItemList code:

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

    The third part is that you need to tell swiftui to permit the user to revise the order of the list. As you show in your example, this is done with the onMove modifier. In that modifier you perform the actions needed to re-order the list in the user's preferred sequence. For example, you could call a convenience function called move so the modifier would read:

    .onMove( perform: move )
    

    Your move function will be passed an IndexSet and an Int. The index set contains all the items in the FetchRequestResult that are to be moved (typically that is just one item). The Int indicates the position to which they should be moved. The logic would be:

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

    Technical reminder: the items stored in revisedItems are classes (i.e., by reference), so updating these items will necessarily update the items in the fetched results. The @FetchedResults wrapper will cause your user interface to reflect the new order.

    Admittedly, I'm new to SwiftUI. There is likely to be a more elegant solution!

    Paul Hudson (Hacking With Swift) has quite a bit more detail. Here is a link for info on moving data in a list. Here is a link for using core data with SwiftUI (it involves deleting items in a list, but is closely analogous to the onMove logic)