Search code examples
swiftuiswiftui-listidentifiable

SwiftUI List support selection with different model types


I am trying to implement a SwiftUI list for a sidebar view which has at least 3 different types of data points: a) a fixed list of enums, b) a list of 'tags' coming from a @FetchRequest using Core Data, c) a similar list of 'groups' coming from a different @FetchRequest.

I'm struggling with how to handle multiple selection with List in this setup. The user should be able to select from different sections (and I get change notifications to fine-tune the handling). I have tried making the 'selection' type to be UUID, and setting the id for each leaf view explicitly, but it doesn't seem to work (I don't the selection highlight).

This is the list I made:

struct CombinedListView: View {
    @FetchRequest(
        entity: CJTag.entity(),
        sortDescriptors: [NSSortDescriptor(keyPath: \CJTag.displayOrder, ascending: true)]
    )
    var tags: FetchedResults<CJTag>

    @FetchRequest(
        entity: PrivateGroups.entity(),
        sortDescriptors: [NSSortDescriptor(keyPath: \PrivateGroups.displayOrder, ascending: true)]
    )
    var groups: FetchedResults<PrivateGroups>

    @State private var selectedItems = Set<UUID>()
    
    var body: some View {
        NavigationView {
            VStack {
                List(selection: $selectedItems) {
                    
                    // section for Tabs
                    Section(header: Text("Main Tabs")) {
                        ForEach(MainTab.allCases, id: \.rawValue) { tab in
                            Text(tab.rawValue)
                                .id(tab.id)
                        }
                    }
                    
                    // Section for Tags
                    if !tags.isEmpty {
                        Section(header: Text("Tags")) {
                            ForEach(tags) { tag in
                                Text(tag.tagName ?? "Tag")
                                    .id(tag.objectID.uriRepresentation().absoluteString) // Directly tag with UUID
                                    .contentShape(Rectangle())
                            }
                        }
                    }
                    // Section for Groups
                    if !groups.isEmpty {
                        Section(header: Text("Groups")) {
                            ForEach(groups) { group in
                                Text(group.groupName ?? "Group")
                                    .id(group.objectID.uriRepresentation().absoluteString)
                                    .contentShape(Rectangle())
                            }
                        }
                    }
                }
                .listStyle(SidebarListStyle())
                .navigationTitle("Selectable List")
            }
        }
    }
}

I know that if I just had NSManagedObjects in the list, I could set the 'selection' type to be NSManagedObjectID and it would work. But I needed it to support a list of enum cases as well.

I tried setting the tag for each row view as well (using the same stuff as id modifier), but that doesn't work either. I'm sure it's a case of mismatched 'types' for selection, but I can't figure out the best setup to accomplish this.

EDIT:

Added code for MainTab:

// Enum for Main Tabs
enum MainTab: String, CaseIterable, Identifiable {
    case home = "Home"
    case favorites = "Favorites"
    case settings = "Settings"
    case profile = "Profile"
    case help = "Help"

    var id: String { rawValue }
    var iconName: String { rawValue.lowercased() }
}

Solution

  • The selections do not work because you have different types for your

    @State private var selectedItems = Set<UUID>()
    

    and the .id(...) which are Strings. They must be of the same type to work.

    So change your selection to match

    @State private var selectedItems = Set<String>()
    

    and I recommend using .tag(...) instead of .id(...).

    And as I mentioned in my comment, NavigationView is deprecated use NavigationStack instead.