4

I'm able to get a Core Data backed flat list working (with no .listStyle modifier) with delete and move functionality.

But when I tried to make list grouped

}.listStyle(GroupedListStyle())

the wheels fall off conceptually. The onDelete modifier parameter has a function signature of IndexSet? -> Void. So I can't pass in the object to be deleted.

onMove is essentially the same problem, except worse. Both modifiers rely on a data source assumed to be a flat array of sequential values which can be accessed by IndexSet subscription. But I can't think how to build a grouped list using a flat datasource.

enter image description here

My view body looks like this:

//I'm building the list using two independent arrays. This makes onDelete impossible to implement as recommended

ForEach(folders, id: \.self) { folder in 
                    Section(header: Text(folder.title) ) {
                        ForEach(self.allProjects.filter{$0.folder == folder}, id: \.self){ project in
                            Text(project.title)
//this modifier is where the confusion starts:
                        }.onDelete(perform: self.delete) 
                    }
                }

            }.listStyle(GroupedListStyle())
    func delete (at offsets: IndexSet) {
        //        ??.remove(atOffsets: offsets)
        //Since I use two arrays to construct group list, I can't use generic remove at Offsets call. And I can't figure out a way to pass in the managed object.

    }

      func move (from source: IndexSet, to destination: Int) {
    ////same problem here. a grouped list has Dynamic Views produced by multiple arrays, instead of the single array the move function is looking for.
        } 
0

3 Answers 3

2

Can't you store the result of the filter and pass that on inside .onDelete to your custom delete method? Then delete would mean deleting the items inside the IndexSet. Is moving between sections possible? Or do you just mean inside each folder? If only inside each folder you can use the same trick, use the stored projects and implement move manually however you determine position in CoreData.

The general idea is the following:

import SwiftUI

class FoldersStore: ObservableObject {
    @Published var folders: [MyFolder] = [

    ]

    @Published var allProjects: [Project] = [

    ]

    func delete(projects: [Project]) {

    }
    func move(projects: [Project], set: IndexSet, to: Int) {

    }
}

struct MyFolder: Identifiable {
    let id = UUID()
    var title: String
}

struct Project: Identifiable {
    let id = UUID()
    var title: String
    var folder: UUID
}

struct FoldersAndFilesView: View {
    var body: some View {
        FoldersAndFilesView_NeedsEnv().environmentObject(FoldersStore())
    }
}

struct FoldersAndFilesView_NeedsEnv: View {
    @EnvironmentObject var store: FoldersStore

    var body: some View {
        return ForEach(store.folders) { (folder: MyFolder) in
            Section(header: Text(folder.title) ) {
                FolderView(folder: folder)
            }
        }.listStyle(GroupedListStyle())
    }
}

struct FolderView: View {
    var folder: MyFolder
    @EnvironmentObject var store: FoldersStore

    func projects(for folder: MyFolder) -> [Project] {
        return self.store.allProjects.filter{ project in project.folder == folder.id}
    }

    var body: some View {
        let projects: [Project] = self.projects(for: folder)

        return ForEach(projects) { (project: Project) in
            Text(project.title)
        }.onDelete {
            self.store.delete(projects: $0.map{
                return projects[$0]
            })
        }.onMove {
            self.store.move(projects: projects, set: $0, to: $1)
        }
    }
}
Sign up to request clarification or add additional context in comments.

6 Comments

Thanks for quick response. Let me play around with it.
Good luck. A single tip: Move everything inside the section inside it's own view, then you can hold let projects = <filtering> on the top-level of var body: some View-method and access it inside .onDelete and .onMove to use and pass along. For this you need store-access inside the subview though to call the move and delete methods.
I added how I thought it could work. Does that fix it for you, or did I misread some requirements? :D The .onDelete uses the indexes pointing to the currently-supplied array, that is why it is calculated on each redraw and always has the projects which can be referenced by .onDelete, that is the neat thing about it. Isolating the objects then is just mapping the indexes to the temporary array and giving the projects to the store to delete.
Nice that it worked out for you! Only problem about it is that it refilters the projects each time(or possibly only if they change, who knows SwiftUI magic, but I doubt it ), that's what I like about Chuck's answer more :-)
This solution absolutely works. But it triggers a bug in Xcode 11, beta 6. Introducing the FolderView struct into an inner ForEach loop breaks UI updates when user in List EditMode = .active. Hopefully by the time the GM is out, that bug will be fixed, and this will be the accepted answer.
|
0

You are correct that the key to doing what you want is getting one array of objects and grouping it appropriately. In your case, it's your Projects. You do not show your CoreData schema, but I would expect that you have a "Projects" entity and a "Folders" entity and a one-to-many relationship between them. Your goal is to create a CoreData query that creates that array of Projects and groups them by Folder. Then the real key is to use CoreData's NSFetchedResultsController to create the groups using the sectionNameKeyPath.

It's not practical for me to send you my entire project, so I will try to give you enough pieces of my working code to point you in the right direction. When I have a chance, I will add this concept into the sample program that I just published on GitHub. https://github.com/Whiffer/SwiftUI-Core-Data-Test

This is the essence of your List:

@ObservedObject var dataSource =
        CoreDataDataSource<Project>(sortKey1: "folder.order",
                                              sortKey2: "order",
                                              sectionNameKeyPath: "folderName")

    var body: some View {

        List() {

            ForEach(self.dataSource.sections, id: \.name) { section in

                Section(header: Text(section.name.uppercased()))
                {
                    ForEach(self.dataSource.objects(forSection: section)) { project in

                        ListCell(project: project)
                    }
                }
            }
        }
        .listStyle(GroupedListStyle())
    }

Portions of CoreDataDataSource:

let frc = NSFetchedResultsController(
            fetchRequest: fetchRequest,
            managedObjectContext: McDataModel.stack.context,
            sectionNameKeyPath: sectionNameKeyPath,
            cacheName: nil)
frc.delegate = self

    public func performFetch() {

        do {
            try self.frc.performFetch()
        } catch {

            let nserror = error as NSError
            fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
        }
    }

    private var fetchedObjects: [T] {

        return frc.fetchedObjects ?? []
    }

    public var sections: [NSFetchedResultsSectionInfo] {

        self.performFetch()
        return self.frc.sections!
    }

    public func objects(forSection: NSFetchedResultsSectionInfo) -> [T] {

        return forSection.objects as! [T]
    }

    public func move(from source: IndexSet, to destination: Int) {

        self.reorder(from: source, to: destination, within: self.fetchedObjects)
    }

3 Comments

1) Entity structure as you describe (one-to-many between Folder and Projects) 2) Use of FetchedResultsController with objects grouped by sectionNameKeyPath parameter is a familiar pattern. 3) The use of generics in your CoreDataDataSource example is aces. While it feels like a bit of a Frankenstein monster to bolt a FetchedResultsController to SwiftUI, the frc is purpose-made for grouped tables out of Core Data. Thanks to both Fabian and Chuck H for getting me unstuck.
I greatly enhanced my GitHub sample project to show you how to implement .onDelete and .onMove when using nested ForEach loops. The solution wasn't totally straight forward. Look at AttributesGroupedView and my greatly enhanced CoreDataDataSource.
thanks for the link to your GitHub CoreData + SwiftUI. I've been referencing it for my use, and so far, it's working really well.
0

If you want to easily delete things from a sectioned (doesn't have to be grouped!) List, you need to take advantage of your nesting. Consider you have the following:

List {
  ForEach(self.folders) { folder in
    Section(header: folder.title) {
      ForEach(folder.items) { project in
        ProjectCell(project)
      }
    }
  }
}

Now you want to set up .onDelete. So let's zoom in on the Section declaration:

Section(header: Text(...)) {
  ...
}
.onDelete { deletions in
  // you have access to the current `Folder` at this level of nesting
  // this is confirmed to work with singular deletion, not multi-select deletion
  // I would hope that this actually gets called once per section that contains a deletion
  // but that is _not_ confirmed
  guard !deletions.isEmpty else { return }

  self.delete(deletions, in: folder)
}

func delete(_ indexes: IndexSet, in folder: Folder) {
  // you can now delete this bc you have your managed object type and indexes into the project structure
}

1 Comment

Thanks, got stuck on this, and temporarily moved away. I'll take a a good look again when I come back to this.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.