Why doesn't SwiftUI reflect a @State property value in a toolbar Menu?

Viewed 66

Testing this piece of code with Xcode 14 beta 5. Everything is working properly (sorting items with an animation, plus saving the selected sortOrder in UserDefaults).

However the if-else condition in the Menu's label seems to be ignored and the label is not updated. Please do you know why? Or what is alternate solution to avoid this?

struct ContentView: View {
    enum SortOrder: String, CaseIterable, Identifiable {
        case forward
        case reverse
        
        var id: Self {
            self
        }
        
        static let `default`: Self = .forward
        
        var label: String {
            switch self {
            case .forward: return "Sort (forward)"
            case .reverse: return "Sort (reverse)"
            }
        }
    }
    
    @AppStorage("sortOrder") var sortOrder: SortOrder = .default {
        didSet {
            sort()
        }
    }
    
    
    @State private var items = ["foo", "bar", "baz"]
    
    @ToolbarContentBuilder
    var toolbar: some ToolbarContent {
        ToolbarItem {
            Menu {
                ForEach(SortOrder.allCases) { sortOrder in
                    Button {
                        withAnimation { self.sortOrder = sortOrder }
                    } label: {
                        // FIXME: doesn't reflect sortOrder value
                        if sortOrder == self.sortOrder {
                            Label(sortOrder.label, systemImage: "checkmark")
                        } else {
                            Text(sortOrder.label)
                        }
                    }
                }
            } label: {
                Label("Actions", systemImage: "ellipsis.circle")
            }
        }
    }
    
    var body: some View {
        NavigationStack {
            List(items, id: \.self) { item in
                Text(item)
            }
            .navigationTitle("Test")
            .toolbar { toolbar }
            .onAppear(perform: sort)
        }
    }
    
    func sort() {
        switch sortOrder {
        case .forward:
            items.sort(by: <)
        case .reverse:
            items.sort(by: >)
        }
    }
}
3 Answers

It is selected but Menu is not updated, assuming in toolbar it should be persistent.

A possible workaround is to force-rebuild menu on sort change, like

Menu {

   // ...

} label: {
    Label("Actions", systemImage: "ellipsis.circle")
}
.id(self.sortOrder)  // << here !!

Tested with Xcode 14b5 / iOS 16

The if-else in your ToolbarItem is working correctly. The problem here is that .labelStyle in a toolbar are defaulted to .titleOnly.

To display the label with both icon and title, you would need to add .labelStyle(.titleAndIcon).

The solution was simply to use a Picker with .pickerStyle modifier set to .menu, instead of a hardcoded Menu.

var toolbar: some ToolbarContent {
    ToolbarItem {
        Picker(selection: $selectedSort) {
            ForEach(Sort.allCases) { sort in
                Text(sort.localizedTitle).tag(sort)
            }
        } label: {
            Label("Sort by", systemImage: "arrow.up.arrow.down")
        }
        .pickerStyle(.menu)
    }
}

Now SwiftUI automatically updates the interface.

Related