Search code examples
swiftswiftui

How to detect a change in the value of “editMode” from a toolbar?


Let's use a selectable List as follows:

import SwiftUI

struct ContentView: View {
    @State private var elements: Set<UUID> = [.init(), .init(), .init()]
    @State private var selection: Set<UUID> = []
    
    @Environment(\.editMode) private var editMode
        
    var body: some View {
        NavigationStack {
            List(
                elements.sorted(),
                id: \.self,
                selection: $selection,
                rowContent: { Text(String(describing: $0)) }
            )
            .toolbar {
                ToolbarItem(placement: .topBarTrailing, content: {
                    EditButton()
                })
                ToolbarItem(placement: .topBarLeading, content: {
                    Button(selection.count == elements.count ? "Unselect All" : "Select All", action: {
                        selection = selection.count == elements.count ? [] : elements
                    })
                    .opacity(editMode?.wrappedValue.isEditing ?? false ? 1 : 0) // FIXME: `editMode` value change is not detected
                })
            }
        }
    }
}

Although changing the value of editMode correctly updates the UI, it is not detected in the toolbar modifier.

Why ?

Tested in Xcode 16 beta 3.


Solution

  • I'm not sure if this is intentional or not. One possible explanation is that SwiftUI fails to (or is not designed to) find an environment value in ContentView as a dependency of a tool bar button.

    This works as expected if you extract the tool bar button into its own View:

    struct SelectAllButton: View {
        let elements: Set<UUID>
        @Binding var selection: Set<UUID>
        
        // importantly, read the environment from this extracted view
        @Environment(\.editMode) private var editMode
        
        var body: some View {
            Button(selection.count == elements.count ? "Unselect All" : "Select All", action: {
                selection = selection.count == elements.count ? [] : elements
            })
            .opacity(editMode?.wrappedValue.isEditing ?? false ? 1 : 0)
        }
    }
    
    ToolbarItem(placement: .topBarLeading) {
        SelectAllButton(elements: elements, selection: $selection)
    }