UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

List with Children

Forums > SwiftUI

Hi. I'm trying to figure out how to set up a hierarchy disclosure list with children based on entries in Core Data.

To start I created a Core Data entity "Item" that contains a "name" attribute as String, a "type" attribute as Integer 16 which can contain 1, 2, 3, 4 or 5. I also set up a relationship in the entity that links to itself as "primary" with a "to one" relationship and the reciprocal "secondary" with a "to many" relationship. Type 1, 2, and 3 are individual primary items with no hierarchy so will just always be a name and type. Type 4 are primary items that can contain secondary items of Type 5 so they have a name, type, and "secondary" links to type 5 entities. Type 5 are always secondary items that belong to one primary type 4 entity so will have a name, type and a single "primary" link to the type 4 entity it belongs to.

I'd like to set up a sidebar list that displays all the type 1 (in alphabetical order), then all the type 2 (in alphabetical order), then all the type 3 (in alphabetical order), and then any type 4 (in alphabetical order) as disclosures (default closed if possible), and then under the type 4 disclosures have the type 5 that are contained within (in alphabetical order). All items able to be selected in the sidebar to display some other content view to the right of the sidebar (including the type 4 so I can select the overall container as well as the sub-contents).

I'm having trouble understanding how to do this. I understand how to set up a struct to represent my data, and understand that it is advisable to set up an optional children variable of an array of the same entity items within the data and make the struct Identifiable. I know how to set up a FetchRequest with sortDescriptors to get my database items into a variable of type FetchedResults<entity> (different from an array of entities) for just the top level items (arranged by type and alphabetically within each type), but where I'm running into a problem is I don't know how to get the type 5 entities I need from the "secondary" relationship of the type 4 items put into an array within my struct for the type 4 items so I can then create a List and have something to pass to the children parameter.

All I can seem to find are tutorials that have the data already set up in a hierarchy as JSON or as hard-coded just to demonstrate a List with Children. I'm having trouble bridging the gap between that and what to do when the data is in Core Data.

Does anyone know of any tutorial that walks through this or can explain how to do this?

2      

I still can't figure it out. Here's my code that I'm using at the moment:

import SwiftUI
import CoreData

struct MainView: View {
    @Environment(\.managedObjectContext) private var viewContext

    @FetchRequest(
        sortDescriptors: [
            NSSortDescriptor(keyPath: \Item.type, ascending: true),
            NSSortDescriptor(keyPath: \Item.name, ascending: true)],
        animation: .default)
    private var items: FetchedResults<Item>

    var body: some View {
        VStack {
            List(items, children: \.secondary) { item in
                NavigationLink {
                    Text(item.name ?? "Unknown Item")
                } label: {
                    Text(item.name ?? "Unknown Item")
                }
//                .onDelete(perform: deleteItems)
            }
            .listStyle(.sidebar)
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    EditButton()
                }
                ToolbarItem {
                    Button(action: addItem) {
                        Label("Add Items", systemImage: "plus")
                    }
                }
            }
        }
    }

    private func addItem() {
        withAnimation {
            let newItem = Item(context: viewContext)
            newItem.name = "Primary Item 1"
            newItem.type = 1

            let newItem2 = Item(context: viewContext)
            newItem2.name = "Primary Item 2"
            newItem2.type = 2

            let newItem3 = Item(context: viewContext)
            newItem3.name = "Primary Item 3"
            newItem3.type = 3

            let newItem4 = Item(context: viewContext)
            newItem4.name = "Primary Item 4"
            newItem4.type = 4

            let newItem5 = Item(context: viewContext)
            newItem5.name = "Secondary Item 1"
            newItem5.type = 5
            newItem5.primary = newItem4

            let newItem6 = Item(context: viewContext)
            newItem6.name = "Secondary Item 2"
            newItem6.type = 5
            newItem6.primary = newItem4

            let newItem7 = Item(context: viewContext)
            newItem7.name = "Secondary Item 3"
            newItem7.type = 5
            newItem7.primary = newItem4

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

    private func deleteItems(offsets: IndexSet) {
        withAnimation {
            offsets.map { items[$0] }.forEach(viewContext.delete)

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

struct MainView_Previews: PreviewProvider {
    static var previews: some View {
        MainView()
    }
}

There are a couple of issues here. The first is that it fails on the "List" line of code because "Key path value type 'NSSet?' cannot be converted to contextural type 'FetchedResults<Item>?'

I'm not really sure how to handle that in this context. I'm not sure if the type could be converted if that would solve the problem. As I mentioned in my first post, the only examples I have seen have hard-coded sample data and put it into an identifiable struct. I've created some structs in my SwiftUI projects to pass a single selected record from one view (usually a list) to another (usually a detail/edit view), but I'm not really sure how I would handle an entire list if it is necessary to do so.

The other issue is that, by having the loop to break things up in the "List" line instead (which seems necessary to pass the children parameter) instead of having a "ForEach" inside the "List" to break things up, the .onDelete is not available. So if I want a hierachy disclosure list in iOS/iPadOS the user can't swipe to delete items in it? That seems rather odd.

There could be other issues in this as well. Since I can't resolve the failure on the "List" line of code I'm not able to run the code to see if anything else breaks, in particular I'm not sure if the list would display correctly in the way I'm fetching the core data (would it include the type 5 items as secondary items under the type 4 item but also as primary items below that in the list or would I have to filter them out with a predicate as part of my fetch request?

2      

@Bnerd  

Have you read/watched this? https://www.hackingwithswift.com/books/ios-swiftui/one-to-many-relationships-with-core-data-swiftui-and-fetchrequest

If yes, then share your manual CoreData Subclasses.

3      

Hacking with Swift is sponsored by Essential Developer

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until April 28th.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

Thanks @Bnerd. I had not considered editing an autogenerated class or even the more recommended extension to a class. The examples I have come across for having an expanding list / list with children have been hard-coded data using a struct so wasn't thinking along the lines of the class. (And honestly, I can usually get by using nil coalescing with my list views and only have added stuff like that (nil coalescing or computed properties) into a struct passed from a selected item in a list to a detail/edit view. So working with a class probably wouldn't have been at the top of my mind even the examples hadn't required a struct for the hard-coded data.

In looking up how to create an extension to a class to refresh my memory since I haven't generally used it (Mark Moeykens has a nice example in his "Core Data Quick Start" visual guide), I did happen to come across something that I wasn't sure about - Core Data automatically makes entites conform to Identifiable. So at least I don't have to worry about that being an issue.

So now it comes down to, I don't know how to create a computed property in an extension on the class to return the expected FetchedResults<Item>. Paul's example in the link you shared is showing how to get from the NSSet of the "to many" relationship to a Set and then into an array.

So if I do something like:

import Foundation
import SwiftUI

extension Item {
    var wrappedName: String {
        name ?? "Unknown Name"
    }

    var secondaryArray: [Item] {
        let set = secondary as? Set<Item> ?? []
        return set.sorted {
            $0.wrappedName < $1.wrappedName
        }
    }
}

I am simply going from an error of "Key path value type 'NSSet?' cannot be converted to contextural type 'FetchedResults<Item>?'" to "Key path value type '[Item]' cannot be converted to contextual type 'FetchedResults<Item>?'

My guess is that I would want to change my secondaryArray computed property to return FetchedResults<Item> instead of [Item], but I'm not really sure where to go from there to actually return that.

2      

@Mike seeks Core Data clarification

I am simply going from an error of Key path value type 'NSSet?' cannot be converted to contextural type 'FetchedResults to "Key path value type '[Item]' cannot be converted to contextual type 'FetchedResults<Item>?'

I think @Nigel answered a similar questions in the forum:

See -> Core Data changing NSSet to Array

His answer addresses one of the universal pieces of advice when it comes to CoreData....Buy Donnie Wals' book.

@twoStraws expressed hope last year that we'd all see a big CoreData update in SwiftUI. But we were all a bit sad it didn't make the cut. Yes, CoreData is not very Swifty and could use a nice big update. Stay tuned??

2      

Thanks @Obelix. Yeah, I was hoping for a big Core Data update in SwiftUI too. Generally Core Data doesn't seem all that bad to me the way it is (which is good because it is likely that if we get a big Core Data update with SwiftUI it might be another of those things that only work going forward and I'd like to hang back at least a version or two in my iOS, iPadOS, and macOS development efforts. I mean, it is weird sometimes and not very SwiftUI-like, but thanks to some great examples, articles, tutorials, visual guides from Paul, Mark, and others things don't generally seem insurmountable (I have not checked out Donnie's book but definitely added it to my list of stuff I want to read once things settle down a bit around here. Mark is currently working on a Core Data Mastery visual guide too that is on my wishlist)

That said, I'm still relatively new to all of this (only a couple of years into SwiftUI and before that I was writing macOS apps with SuperCard - not the usual path for people who are learning and migrating to SwiftUI) so I'm not sure, even if there is a solution to the above problem, that I will necessarily understand it at this point. I was hoping the answer was something obvious that I was just missing, but the lack of finding anything specifically addressing the problem did have me concerned (on the other hand, it wouldn't be the first time an answer was right under my nose and I just wasn't seeing it so I decided to ask here).

In the end the matter might be academic. While I still am very curious if there is some relatively simple way to overcome it and what that looks like, the lack of having a way to swipe to delete items in an expandable list / list with children presents something of an issue (these are items - and subitems - that users are going to create and may wish to delete). Fortunately there is often more than one way to solve matters of interface. Sometimes many. One of the things I actually love about developing apps is getting into an end-user mindset and exploring different UI options.

At this point, I'm thinking of just putting the four "primary" types into the sidebar (filtering out the fifth type) and when the user selects the fourth type, present a view of just the type 5 subitems that belong to it and allow them to be selected in that view and then navigate to a further detail view from there (similar to the view you would get directly from the type 1, 2, and 3 items in the sidebar). Sure it does add another view between and that isn't as immediate as having that full hierarchy right in the sidebar but it does simplify the sidebar and lets me return to using a ForEach so I can enable .onDelete. It is somewhat different from my SuperCard-based app that my SwiftUI project is replacing too but, that isn't necessarily a bad thing here. The intermediate view sitting before the sub-content data might actually end up making more sense in the context of what I'm trying to do and presents some other possible advantages the more I think about it.

2      

The problem here isn't really Core Data, it's the List. For a hierarchical List, the children need to be the same type as the parent. A FetchedResults<Item> is not the same thing as an [Item].

But since FetchedResults conforms to Sequence you should be able to just do this (assuming that secondaryArray is of type [Item]):

List(Array(items), children: \.secondaryArray) { item in

but you should definitely not take my word for it and instead try it out yourself.

3      

Thanks @roosterboy. Love it. If you can't make the second parameter conform to the first, make the first parameter conform to the second. 🙂 Brilliant!

That does indeed work with a couple of changes. I had to change my VStack in the body to a NavigationView so the toolbar would show up and I could use my add button to create my test data (that was an obvious mistake on my part but didn't spot it until I was able to run the project). I had to change the extension to the Item class for the secondaryArray computed property:

var secondaryArray: [Item]? {
        let set = secondary as? Set<Item> ?? nil

        if set != nil {
            return set!.sorted {
                $0.wrappedName < $1.wrappedName
            }
        } else {
            return nil
        }
    }

Revealed some other things about expanding lists. Yes I had to add a predicate to my fetch request to filter out the type 5 items or they appear as both secondary items correctly inside the type 4 item and as primary items below the type 4 item. That was easy enough:

    @FetchRequest(
        sortDescriptors: [
            NSSortDescriptor(keyPath: \Item.type, ascending: true),
            NSSortDescriptor(keyPath: \Item.name, ascending: true)],
        predicate: NSPredicate(format: "type != 5"),
        animation: .default)
    private var items: FetchedResults<Item>

But there are some other issues. I thought the extension on class might take care of this by returning nil when there is no Set and then maybe the expanding list would handle things, but my type 1, 2, 3 and 5 items all appear in the list with disclosures even though they do not (and never will) have child data. I think this is my mistake somewhere as I don't recall ANY of the examples or tutorials for this type of list showing disclosures in items that didn't have any children.

The other issue comes down to using the List to loop through the array and the lack of being able to use .onDelete since there is no ForEach to apply the modifier to.

Still, I have a working experiment project now vs. red stop signs... so definite progress.

2      

@Bnerd  

Ok I have an idea that I believe could solve your problem. You need to restructure your CoreData as following:

  1. Make an Entity called ItemList, add an attribute name:String (it really doesn't matter unless you want to make several)
  2. Make an Entity called Item, add attributes , name:String & type:Int16

Work on the relationships:

  1. Make a relationship for ItemList to Item, To Many (One ItemList can have many Items)
  2. Make a relationship for Item to ItemList, To One (One Item belongs to one ItemList)

Generate your custom classes, as Paul did it in his example (https://www.hackingwithswift.com/books/ios-swiftui/one-to-many-relationships-with-core-data-swiftui-and-fetchrequest )

The custom class of ItemList will allow you to generate your ItemArray (just the way you did it above in your first response to me) Then you just use this array as you want...

2      

Thanks @Bnerd, but I'm not entirely following how this would solve the problem. The expanding list I'm trying to create would appear something like this:

Type 1 Item 1
Type 1 Item 2
Type 2 Item 1
Type 3 Item 1
Type 3 Item 2
Type 3 Item 3
Type 4 Item 1 ▼
       Type 5 Item 1
       Type 5 Item 2
       Type 5 Item 3
Type 4 Item 2 ▶︎

Each item in that list can be selected to display a detail view (including the Type 4 items which have a slightly different view than the Type 1, 2, 3, and 5).

The issue right now is that the list with children / expanding list appears like this:

Type 1 Item 1 ▶︎
Type 1 Item 2 ▶︎
Type 2 Item 1 ▶︎
Type 3 Item 1 ▶︎
Type 3 Item 2 ▶︎
Type 3 Item 3 ▶︎
Type 4 Item 1 ▼
       Type 5 Item 1 ▶︎
       Type 5 Item 2 ▶︎
       Type 5 Item 3 ▶︎
Type 4 Item 2 ▶︎

It implies that there could be sub-contents of the Type 1, 2, 3, and 5 items even though that is never the case and clutters the list with a bunch of useless disclosures. I thought that if the optional data (secondary relationship) was nil the expanding list might realize that and not display the disclosures so either it doesn't work that way (although there were no disclosures on the hard-coded sub-items in the tutorials such as https://www.hackingwithswift.com/quick-start/swiftui/how-to-create-expanding-lists), or I've messed this up in the process of trying to get it to work with Core Data (most likely).

2      

@Bnerd  

Hmm, what if you use NavigationStack instead of NavigationLinks? You could trigger your navigation with .OnTapGesture

2      

@Bnerd, sorry, I think my example done as text is throwing things off a bit. The arrows I included are not the gray navigation arrows, they are blue (or the accentColor) and are meant for disclosure (they rotate from pointing to the right to pointing down when you tap on them and the sub-items are displayed and then rotate again from pointing down to the right when you tap them to hide the sub-items). On iOS 16 the disclosure arrows in an expanding list have a greater priority and the gray navigation arrows are not shown if navigation links are used (thankfully).

Annoyingly if you run my code on iOS 15, the expanding list doesn't seem to prioritize things at all and opts instead to show BOTH arrows. So you get the gray navigation arrows pointing right AND the blue(or the accentColor) disclosure arrows (pointing to the right or down). Yeah, that just looks really strange and cluttered to me.

That aside, fixing having visible gray navigation arrows doesn't solve the blue disclosure arrows appearing on every item (which is the problem I was getting at).

At this point I've already decided to go with a non-expanding sidebar list in my project instead of an expanding list due to the numerous issues (with delete, with disclosures showing up on items that don't have anything to disclose, and yeah, I can add the double-arrow issue under iOS 15 to the list too) and instead I will add an intermediate view so when the user selects a type 4 item it will bring up the list of type 5 items in another view, and if you tap on a type 5 item in that view you will get at the detail view which you would get by tapping directly on type 1, 2, or 3 items in the sidebar. In the context of how I'm using it in my project, this intermediate view might actually end up being a better way to go for both me in terms of code and for users in terms of following the logic of the UI.

But I'm still interested in hearing ideas for overcoming the obstacles with the expanding list using Core Data because it might come in handy one day in some other context. For all I know, this might just be the limitations of using an expanding list / list with children as it exists currently in SwiftUI and I've taken it as far as possible with the help of people here and just have to wait for changes/improvements in iOS 17 and/or beyond. My SwiftUI experience is new enough though that I'm not comfortable making any such determination and will continue to monitor this thread to see if any other solutions/answers are posted.

2      

Hacking with Swift is sponsored by Essential Developer

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until April 28th.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

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.