swiftuiswiftui-list

Creating a Nested Tree List View in SwiftUI


I have a class that I would like to use to populate a self-nesting tree style list. Here is the basic definition:

final class Item : Hashable, Codable, Identifiable {
    var name: String
    var brand: String?
    var liked: Bool?
    var notes: String?
    var children: [Item]?

    init(categoryName: String, items: [Item]?) {
        self.name = categoryName
        if let items {
            self.children? = []
            self.children!.insert(contentsOf: items, at: 0)
        }
    }

    init(productName: String, liked: Bool, notes: String) {
        self.name = productName
        self.brand = ""
        self.liked = liked
        self.notes = notes
    }

    init(productName: String, brand: String, liked: Bool, notes: String) {
        self.name = productName
        self.brand = brand
        self.liked = liked
        self.notes = notes
    }
}

Even though the goal is to eventually pull this data from JSON or CoreData or something (I'm VERY new), for the purposes of testing, I have manually built some data like so:

let item1 = Item(productName: "Crispy Honey Shrimp", brand: "P.F. Changs", liked: true, notes: "Goold flavor and texture")
let item2 = Item(productName: "Bourbon Chicken Pasta (bag)", brand: "Zatarain's", liked: true, notes: "Tasty, easy to make")
let item3 = Item(productName: "Bourbon CHicken Pasta (box)", brand: "Zatarain's", liked: false, notes: "Wrong pasta, bad sauce")
let item4 = Item(productName: "Fries", liked: true, notes: "Well seasoned. Cronch")
let item5 = Item(productName: "Texas Cheesesteak Plate", liked: true, notes: "")
let cat7 = Item(categoryName: "Checker's", items: [item4])
let cat6 = Item(categoryName: "Waffle House", items: [item5])
let cat5 = Item(categoryName: "Fast Food", items: [cat7])
let cat4 = Item(categoryName: "Dine-In", items: [cat6])
let cat3 = Item(categoryName: "Restaraunts", items: [cat4, cat5])
let cat2 = Item(categoryName: "Frozen Food", items: [item1, item2, item3])
let cat1 = Item(categoryName: "Grocery", items: [cat2])
let items = [cat1, cat3]

Finally, the view looks like this:

struct ItemList: View {
    var body: some View {
        List(items, children: \.children) { row in
            HStack {
                Text(row.name)
            }
        }
    }
}

The simulator crashes, and it seems to have difficulties with the data model? I guess? Its difficult for me to parse. A pastebin of the crash report is at https://pastebin.com/mpXdcQ0a

I'm a C# dev, and SwiftUI is a VERY new paradigm for me. And I'm definitely struggling. If I am obviously going wrong somewhere - even if its just advice, pointing out anti-patterns, not actual answers to the question - I welcome any knowledge. Thanks.


Solution

  • Try this basic approach in SwiftUI, using struct Item, with a custom id to make it Identifiable. Also using a @State var items: [Item] = [] in the View and setting its data in .onAppear{...}

    struct Item : Hashable, Codable, Identifiable {  // <--- here
        let id = UUID()  // <--- here
        
        var name: String
        var brand: String?
        var liked: Bool?
        var notes: String?
        var children: [Item]?
        
        enum CodingKeys: String, CodingKey {  // <--- here, note no id
            case name,brand,liked,notes,children
        }
        
        init(categoryName: String, items: [Item]?) {
            self.name = categoryName
            if let items {
                self.children = []
                self.children?.append(contentsOf: items)  // <--- here
            }
        }
        
        init(productName: String, liked: Bool, notes: String) {
            self.name = productName
            self.brand = ""
            self.liked = liked
            self.notes = notes
        }
        
        init(productName: String, brand: String, liked: Bool, notes: String) {
            self.name = productName
            self.brand = brand
            self.liked = liked
            self.notes = notes
        }
    }
    
    struct ContentView: View {
        @State var items: [Item] = []  // <--- here
        
        var body: some View {
            List(items, children: \.children) { row in
                HStack {
                    Text(row.name)
                }
            }
            .onAppear {
                let item1 = Item(productName: "Crispy Honey Shrimp", brand: "P.F. Changs", liked: true, notes: "Goold flavor and texture")
                let item2 = Item(productName: "Bourbon Chicken Pasta (bag)", brand: "Zatarain's", liked: true, notes: "Tasty, easy to make")
                let item3 = Item(productName: "Bourbon CHicken Pasta (box)", brand: "Zatarain's", liked: false, notes: "Wrong pasta, bad sauce")
                let item4 = Item(productName: "Fries", liked: true, notes: "Well seasoned. Cronch")
                let item5 = Item(productName: "Texas Cheesesteak Plate", liked: true, notes: "")
                let cat7 = Item(categoryName: "Checker's", items: [item4])
                let cat6 = Item(categoryName: "Waffle House", items: [item5])
                let cat5 = Item(categoryName: "Fast Food", items: [cat7])
                let cat4 = Item(categoryName: "Dine-In", items: [cat6])
                let cat3 = Item(categoryName: "Restaraunts", items: [cat4, cat5])
                let cat2 = Item(categoryName: "Frozen Food", items: [item1, item2, item3])
                let cat1 = Item(categoryName: "Grocery", items: [cat2])
                
                items = [cat1, cat3]  // <--- here
            }
        }
        
    }
    

    Once you start using persistence, for example SwiftData, you will have to adjust the code, such as:

    @Model class Item: Codable, Identifiable { ... }

    and in the View:

    @Query(sort: \.name, order: .reverse) var items: [Item]