2

I have a SwiftUI list, defined in a typical fashion:

struct SettingsView: View
{
    @State private var selectedCategory: SettingsCategory? = .general

    List(SettingsCategory.allCases, id: \.self, selection: $selectedCategory) { category in
        [...]
    }
}

In this case, the List is a table of "categories" for a settings area in my UI. The SettingsCategory is an enum that defines these categories, and the UI ends up looking like this:

enter image description here

It is not appropriate for this list to have an empty selection; a category should always be selected. In AppKit, it was trivially easy to disable an empty selection on NSTableView. But in SwiftUI, I've been unable to find a way to disable it. Anytime I click in the empty area of the list, the selection is cleared. How can I stop that?

selectedCategory must be an Optional or the compiler vomits all over itself.

I can't use willSet/didSet on selectedCategory because of the @State property wrapper. And I can't use a computed property that never returns nil because the List's selection has to be bound.

I also tried this approach: SwiftUI DatePicker Binding optional Date, valid nil

So, what magical incantation is required to disable empty selection in List?

5
  • I am not sure this is the way you want to go. In order for selection on a List to work, you have to have the List in editMode. Once the list is in editMode the initial selection is selected. You can roll your own implementation with List pretty easily, just don't use selection. It isn't meant to be for menus. Commented Feb 14, 2022 at 2:01
  • @Yrb that limitation is for the toy platforms: iOS and tvOS. On macOS, no such limitation exists. Commented Feb 14, 2022 at 2:10
  • Regardless of whether it is on the "toy" platforms or not, selection was not intended to be a menu picker. In the time it took you to ask this question, you could have rolled your own solution. But, keep trying to make this work, by all means. Commented Feb 14, 2022 at 2:12
  • @yrb I’d love to see your proposed solution. Using a NSTableView to power a UI like this has been standard practice on the Mac for quite literally decades. If there’s a better approach than List, please do share. Commented Feb 14, 2022 at 2:33
  • List also comes with lots of freebies that would be tedious to implement manually: the correct selection color based on the NSWindow state (main/key, background), the automatic coloring of text/icon in selected rows, etc. And List selections are absolutely appropriate: lots of Mac UI changes based on selections in a List. Think of a simple master/detail UI with an inspector on the right that shows details about the stuff selected in the list. Commented Feb 14, 2022 at 2:44

2 Answers 2

3

One solution would be to set the selection back to the original one if the selection becomes nil.

Code:

struct ContentView: View {
    @State private var selectedCategory: SettingsCategory = .general

    var body: some View {
        NavigationView {
            SettingsView(selectedCategory: $selectedCategory)

            Text("Category: \(selectedCategory.rawValue.capitalized)")
                .navigationTitle("App")
        }
    }
}
enum SettingsCategory: String, CaseIterable, Identifiable {
    case destination
    case general
    case speed
    case schedule
    case advanced
    case scripts

    var id: String { rawValue }
}
struct SettingsView: View {
    @Binding private var selectedCategory: SettingsCategory
    @State private var selection: SettingsCategory?

    init(selectedCategory: Binding<SettingsCategory>) {
        _selectedCategory = Binding<SettingsCategory>(
            get: { selectedCategory.wrappedValue },
            set: { newCategory in
                selectedCategory.wrappedValue = newCategory
            }
        )
    }

    var body: some View {
        List(SettingsCategory.allCases, selection: $selection) { category in
            Text(category.rawValue.capitalized)
                .tag(category)
        }
        .onChange(of: selection) { [oldCategory = selection] newCategory in
            if let newCategory = newCategory {
                selection = newCategory
                selectedCategory = newCategory
            } else {
                selection = oldCategory
            }
        }
    }
}
Sign up to request clarification or add additional context in comments.

Comments

2

you could try adding .onChange to the List, such as:

.onChange(of: selectedCategory) { val in
    if val == nil {
        selectedCategory = .general // <-- make sure never nil
    }
}

1 Comment

This works! It's a little kludgy because the selection "shudders" to empty and then back to selected, which entails extra view drawing, but this at least works. I still feel like the ultimate solution is a custom @State wrapper that disallows nil values in the setter, but this is at least something!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.