WWDC21 SALE: Save 50% on all my Swift books and bundles! >>

Coredata and Hierarchical List

Forums > SwiftUI

I keep circling back to this issue, and I hope someone can clarify it for me. I can accomplish this with a combination of List and ForEach, but it seems like a hack and there should be a simple straight forward solution.

The idea is I have an Entity that is elements of both their own children and it's parents. Every element can have more zero or more elements as it's children (nested) and it can also have one other element that is it's parent (except for the root element which has no parent). So, the Entity has a relationship with itself as parent, and an inverse relationship of children.

Entity: Element

id -> UUID
name -> String

relationship -> destination -> inverse

children -> Element -> parent -> To One
parent -> Element -> children -> To Many

I am running the latest beta of both XCode and MacOS 11, and the latest SwiftUI. I am building for MacOS, not iOS or iPadOS:

I want to read that entity in HomeView (ContentView):

    @Environment(\.managedObjectContext) var moc

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

Then I want to create/display a hierarchtical list - which it would seem should be this simple:

        NavigationView {

            VStack(spacing: 20) {

                //
                //                Text("Selected Item = \(model.getItem(uuid: selection)?.name ?? "n/a")")
                //                    .foregroundColor(selection == nil ? Color.secondary : Color.green)
                //                    .font(.title2)
                //

                List(elements, id: \.id, children: \.children, selection: $selection) { element in

                    NavigationLink(destination: ManuscriptView(element: element)) {
                        Label(element.xName, systemImage: element.isFolder ? "folder.fill" : "envelope")
                            .tag(element.id)
                    }
                }
                .listStyle(SidebarListStyle())
                .frame(width: 300)
            }
        }

This works if I leave out the children argument, but that only displays the list of top level parents. If I include the children argument, I get an error:

"Key path type "NSSet?" cannot be converted to contextual type 'Fetchresults<Element>?'

This makes since, because the List is expecting a children argument that is an Array of "elements" (whatever class the first argument is).

I obviously can convert the NSSet returned from the FetchRequest into an array of elements, but the problem with that is that I am then sticking my finger in and stirring the ManagedObject system in Coredata, since any changes that occur to those elements in the array become MY responsibility to track.

It seems like such a basic fundamental pattern for SwiftUI using Coredata, but I can't figure out a "clean" solution.

Any suggestions would be greatly apprecaited. (I expect the solution is going to be to wait until Apple accomodates this - sigh...)

TIA,

Frank

   

I actually had the same problem with creating a dynamic sidebar for iOS and iPadOS. I got this working but you need to do a whole lot more to get around the problem that children have to be the same type as the parent. I followed Paul's tutorial on this, of course, this tutorial doesn't cover CoreData.

  • I came up with an enum where the cases match the name of the entities (String, CaseIterable)
  • A protocol for the NSManagedObject with only one function static func generateMenuItems(_ context: NSManagedObjectContext) -> [MenuItem] This function basically creates the MenuItem including children for your dynamic list. But it does it in the CoreData entity.
  • The struct with the actual MenuItem with children of type [MenuItem].

But unfortunatley, it's a whole lot more code than only a single line.

1      

Thank you,

I have consideered going that route, and what I am having troule with is maintaining a single source of truth. I always end up having to keep the view model in sync with the coredata model, and that is not (if I understand it correctly) the "right way" for SwiftUI.

I am trying to come up with an extension to my Elements class (core data model) what Paul calls "wrapped" values. Basically Set/Gets for each attribute. The one that give me indigestion (of course) is the "Element.children" which is a relationship that is one to many and therefore ends up being an NSSet.

I need/want to transparently end up with childrenUnwrapped being an Array that doesn't give the List... indigestion.

I have gotten to this, but it still feels clunky:

This in my Element Extension:

    public var childrenArray: [Element]? {
        let set = children as? Set<Element> ?? []
        return set.sorted {
            $0.xName < $1.xName
        }
    }

This is the test body:

    var body: some View {

        NavigationView {

            VStack(spacing: 20) {

                 List(elements) { element in

                    let children = element.childrenArray ?? []

                    List(children, id: \.id, children: \.childrenArray, selection: $selection) { child in

                        NavigationLink(destination: ManuscriptView(name: child.xName)) {
                            Label(child.xName, systemImage: child.childrenArray != nil ? "folder.fill" : "envelope")
                                .tag(child.id)
                        }
                    }
                }
            }
            .listStyle(SidebarListStyle())
            .frame(width: 300)
        }
    }

The result works - the outer list lists all the "top level parents" and the inner list handles each of thier children.

When I say it works, I mean it iterates through the items, but it looses the heirachtical relationship between the top level elements and the children.

   

Save money with our WWDC sale!

SAVE 50% To celebrate WWDC21, all our books and bundles are half price, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.

Save 50% on all our books and bundles!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.