swiftuiswiftui-list

How to select and display both category and subcategory from hierarchical list with OnTapGesture


I have issue with the code to get both category and subCategory name from hierarchical list when it selected via onTapGesture. For example when I select housing category which is without any subcategory, The onTapGesture is correctly displaying the category name and its icon in the Selected Category Section. However when I select the category list with the subCategory, the onTapGesture is only selecting subCategory item name from the list .

However I am looking for the updated code so whenever I select the subCategory , it will also select the category name and icon for this particular subCategory.

For example if I select the Housing category I want to display following Items only in the the Selected Category Section:

HStack {
    Image(systemName: item.icon) // this will be housing icon
    Text(item.name)              // this will be housing name 
}

And if I select the the telephone subcategory from the Bills Category , I want to display the following items only in the Selected Category Section.

HStack {
    Image(systemName: item.icon) // this will be bills icon
    Text(item.name)              // this will be bills category name
    Text(item.subCategoryName)   // this will be Telephone SubCategory name which I have the issue with the code to get subCategory name. 
}

Following is the code:

import SwiftUI
struct Category: Hashable, Identifiable {
    var name: String
    let icon: String
    var subCategory: [Category]? = nil
    let id = UUID()

    static func preview() -> [Category] {
        [Category(name: "Bills",
                  icon: "lightbulb",
                  subCategory: [Category(name: "Electricity",
                                      icon: "lightbulb"),
                             Category(name: "Telephone",
                                     icon: "lightbulb")]),
         Category(name: "Transport",
                 icon: "car",
                subCategory: [Category(name: "Taxi",
                                    icon: "car")]),
         Category(name: "Housing", icon: "house")]
    }
}

struct ContentView: View {
    
    @State private var items = Category.preview()
    @State private var name: String = ""
    @State private var icon: String = ""
    
    var body: some View {
        NavigationView {
            Form {
                Section {
                    List(items, children: \.subCategory) { item in
                        
                        HStack {
                            Image(systemName: item.icon)
                            Text(item.name)

                        }
                        .onTapGesture {
                            icon = item.icon
                            name = item.name
                        }
                    }
                } header: {
                    Text("Category List")
                }
                Section {
                    HStack {
                        Image(systemName: icon) // Category icon
                        Text(name) // Category Name
                 //  Code to display subCategory name here for the selected 
                    }
                } header: {
                    Text("Selected Category")

                }
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Solution

  • Based on your edits, you want to show all subcategories for your main categories. You need to track the selected Category and not Category's parameters. Because Category has an infinite amount of levels, you need to create a view that displays name and icon and, if subcategories exist, calls itself recursively to handle each level.

    struct ContentView: View {
        @State private var items = Category.preview()
        // You need to keep track of the Category, not the parameters on the Category.
        // This is nil until a Category is selected.
        @State private var selectedCategory: Category?
        
        var body: some View {
            NavigationView {
                Form {
                    Section {
                        List(items, children: \.subCategory) { item in
                            
                            HStack {
                                Image(systemName: item.icon)
                                Text(item.name)
                            }
                            .onTapGesture {
                                selectedCategory = item
                            }
                        }
                    } header: {
                        Text("Category List")
                    }
                    
                    Section {
                        // this unwraps the optional selectedCategory
                        if let selectedCategory {
                            CategoryView(selectedCategory: selectedCategory)
                        } else {
                            Text("Please select a category")
                        }
                    } header: {
                        Text("Selected Category")
                        
                    }
                }
            }
        }
    }
    
    struct CategoryView: View {
        
        let selectedCategory: Category
        
        var body: some View {
            VStack(alignment: .leading) {
                // this handles the top level.
                HStack {
                    Image(systemName: selectedCategory.icon)
                    Text(selectedCategory.name)
                }
                if let subCategories = selectedCategory.subCategory {
                    // this handles the subcategories
                    ForEach(subCategories) { subCategory in
                        CategoryView(selectedCategory: subCategory)
                    }
                }
            }
        }
    }
    

    Edit:

    I created this for demonstration purposes only. In order to do what you want, you will need to change your underlying data structure. I would recommend implementing this with CoreData where you can more easily establish relationships between your objects. Otherwise, you are looking at a linked list. I updated your model to handle the UI, and changed the preview around to set it up as well, but hhis really is not a great data model and is for demonstration only.

    struct ContentView: View {
        
        @State private var items = Category.preview()
        // You need to keep track of the Category, not the parameters on the Category.
        // This is nil until a Category is selected.
        @State private var selectedCategory: Category?
        
        var body: some View {
            NavigationView {
                Form {
                    Section {
                        List(items, children: \.subCategory) { item in
                            
                            HStack {
                                Image(systemName: item.icon)
                                Text(item.name)
                            }
                            .onTapGesture {
                                selectedCategory = item
                            }
                        }
                    } header: {
                        Text("Category List")
                    }
                    
                    Section {
                        // this unwraps the optional selectedCategory
                        if let selectedCategory {
                            // THis will only go up one node
                            if let superCategory = selectedCategory.superCategory?.first {
                                CategoryView(selectedCategory: superCategory)
                            } else {
                                CategoryView(selectedCategory: selectedCategory)
                            }
                        } else {
                            Text("Please select a category")
                        }
                    } header: {
                        Text("Selected Category")
                        
                    }
                }
            }
        }
    }
    
    struct CategoryView: View {
        
        let selectedCategory: Category
        
        var body: some View {
            VStack(alignment: .leading) {
                // this handles the top level.
                HStack {
                    Image(systemName: selectedCategory.icon)
                    Text(selectedCategory.name)
                }
                if let subCategories = selectedCategory.subCategory {
                    // this handles the subcategories
                    ForEach(subCategories) { subCategory in
                        CategoryView(selectedCategory: subCategory)
                    }
                }
            }
        }
    }
    
    struct Category: Hashable, Identifiable {
        var name: String
        let icon: String
        private (set) var superCategory: [Category]?
        var subCategory: [Category]?
        let id = UUID()
        
        static func preview() -> [Category] {
            [Category(name: "Bills",
                      icon: "creditcard",
                      subCategory: [Category(name: "Electricity",
                                             icon: "lightbulb",
                                             superCategory: [Category(name: "Bills",
                                                                      icon: "creditcard",
                                                                      subCategory: [Category(name: "Electricity",
                                                                                             icon: "lightbulb")])]
                                             
                                            ),
                                    Category(name: "Telephone",
                                             icon: "phone",
                                             superCategory: [Category(name: "Bills",
                                                                      icon: "creditcard",
                                                                      subCategory: [Category(name: "Telephone",
                                                                                             icon: "phone")])]
                                            )]),
             Category(name: "Transport",
                      icon: "airplane",
                      subCategory: [Category(name: "Taxi",
                                             icon: "car",
                                             superCategory: [Category(name: "Transport",
                                                                      icon: "airplane",
                                                                      subCategory: [Category(name: "Taxi",
                                                                                             icon: "car")])
                                             ]
                                             
                                            )]),
             Category(name: "Housing", icon: "house")]
        }
        
        mutating func setSupercategory(_ superCategory: Category) {
            // We don't want the actual Category as it would cause UI issues
            self.superCategory = [
                Category(name: superCategory.name,
                         icon: superCategory.icon,
                         superCategory: superCategory.superCategory,
                         subCategory: [self])]
        }
    }