Search code examples
core-dataswiftuirelationshiphierarchical-data

SwiftUI: Hierarichal, Selectable List


I've been working on a problem for the last couple of days and I can't seem to figure out how to approach it.

I'd like to achieve an hierarichal system of material-groups where the leaf would be a specific material.
I basically have two CoreData entities, Material and MaterialGroup.

MaterialGroup has 3 relationships: subGroup (MaterialGroups that belong to a specific MaterialGroup), superGroup (the inverse of subGroup) and subMaterial (the leaf of the hierarchy).

The ideal scenario which I'd like to accomplish is a selectable list of MaterialGroups, for every selection I'd have 3 options:
(1) - Create MaterialGroup on the same level of the hierarchy
(2) - Create subGroup for selection
(3) - Create a Material for MaterialGroup

A the moment I can create these hierarchies but I cannot display them, I just have a plain selectable list with no depth. I cannot figure out how to make the lists expandable and selectable at the same time.
Kind of like this.

I've also wondered how to then select the Material when I worked my way to a leaf. If I were to use something like List(materialGroupArray, selection: $materialGroupSelection), I couldn't select the Material.

I'd be grateful for any tips.
Have a nice day.


Solution

  • Here is a possible approach for a hierarchical list:

    enter image description here

    import SwiftUI
    
    class Material {
        let name: String
        init(name: String) {
            self.name = name
        }
    }
    
    class MaterialGroup: Identifiable {
        let id: String
        var subGroup: MaterialGroup?
        let subMaterial: Material?
    
        init(id: String, subGroup: MaterialGroup?, subMaterial: Material?) {
            self.id = id
            self.subGroup = subGroup
            self.subMaterial = subMaterial
        }
    }
    
    let group1 = MaterialGroup(id: "G1",subGroup: nil, subMaterial: Material(name: "M1"))
    let group2 = MaterialGroup(id: "G2",subGroup: nil, subMaterial: Material(name: "M2"))
    let group3 = MaterialGroup(id: "G3",subGroup: group2, subMaterial: nil)
    let group4 = MaterialGroup(id: "G4",subGroup: group3, subMaterial: nil)
    
    let demoData: [MaterialGroup] = [
        group1,
        group4,
        MaterialGroup(id: "G5",subGroup: nil, subMaterial: Material(name: "M5"))
    
    ]
    
    struct GroupView: View {
        let group: MaterialGroup
        @State var showChildRow = false
        @State var selected = false
    
        var hasChildren: Bool {
            group.subGroup != nil
        }
    
        var displayName: String {
            if let material = group.subMaterial {
                return material.name
            } else {
                return group.id
            }
        }
    
        var body: some View {
            HStack {
                if hasChildren {
                    Image(systemName: showChildRow ? "chevron.down" : "chevron.right")
                }
    
                Text("\(displayName)").font(.caption2)
                Spacer()
            }.padding()
            .transition(.scale)
            .background(RoundedRectangle(cornerRadius: 15).fill(selected ? Color.red.opacity(0.1) : Color.black.opacity(0.1)))
            .onTapGesture {
                if hasChildren {
                    withAnimation {
                        showChildRow.toggle()
                    }
                } else {
                    // You could use a closure here
                    // onSelect: (Material) -> Void
                    selected.toggle()
                }
            }
    
            if hasChildren && showChildRow {
                GroupView(group: group.subGroup!)
                    .padding(.leading)
            }
        }
    }
    
    
    struct ContentView: View {
        var body: some View {
            ScrollView {
                ForEach(demoData) { group in
                    VStack(alignment: .leading) {
                        GroupView(group: group)
                    }
                }
            }.padding(.horizontal)
        }
    }
    
    struct ContentView_Previews: PreviewProvider {
        static var previews: some View {
            ContentView()
        }
    }