Can't access isEditing property of editMode

Accessing the isEditing property of environment value editMode leads to a compiler error.


In the code below I'm getting 'Generic parameter 'S' could not be inferred' applied to the Button


If I remove && !mode?.isEditing it compiles OK. I've tried juggling things around, but it always finds some problem. It looks like a bug to me either in @Environment or ViewBuilder. Any suggestions?


Here's the full struct:

struct ProjectView: View {
    @Environment(\.editMode) var mode
    @State private var canAddProject = false
    @State private var showAlert = false
    
    var projects: FetchedResults
    var alert: Alert {
        Alert(title: Text("Write Now"), message: Text("Please enter a project name"), dismissButton: .default(Text("Dismiss")))
    }
    
    var body: some View {
        NavigationView {
            List {
                if !canAddProject && !mode?.isEditing {
                    Button("Add New Project") {}
                        .onTapGesture {
                            self.canAddProject.toggle()
                    }
                }
                if canAddProject {
                    AddProject(canAddProject: $canAddProject, showAlert: $showAlert)
                }
                
                ProjectList(projects: projects)
            }
            .listStyle(GroupedListStyle())
            .navigationBarTitle(Text("Write Now"))
            .navigationBarItems(trailing: EditButton())
        }
        .navigationViewStyle(StackNavigationViewStyle())
        .alert(isPresented: $showAlert, content: { self.alert })
    }
}

Accepted Reply

Thanks for this Jim. I tried this with the following:


struct ContentView: View {
    @Environment(\.editMode) var mode

    var body: some View {
        NavigationView {
            List {
                if self.mode?.wrappedValue.isEditing ?? true {
                    Text("Editing")
                }
                else {
                    Text("Not Editing")
                }
            }
            .navigationBarTitle(Text("Test Edit"))
            .navigationBarItems(trailing: EditButton())
        }
    }
}


Unfortunately this didn't work - it just displayed "Not Editing" all the time. Suspecting this had to do with the edit button and mode being declared at the same level I refactored it to the following, which does work.


struct ContentView: View {
    
    var body: some View {
        NavigationView {
            List {
                DisplayEditor()
            }
            .navigationBarTitle(Text("Test Edit"))
            .navigationBarItems(trailing: EditButton())
        }
    }
}

struct DisplayEditor: View {
    @Environment(\.editMode) var mode
    
    var body: some View {
        if self.mode?.wrappedValue.isEditing ?? true {
            return Text("Editing")
        }
        else {
            return Text("Not Editing")
        }
    }
}

Replies

The value of your `mode` property is actually a `Binding<EditMode?>`. The compiler uses the @Environment wrapper to generate helper properties to reach inside an `Environment<Binding<EditMode?>>>`. So where an `@Binding` would provide this:


// @State var mode: EditMode? = nil
var _mode: State<editmode?> = State(wrappedValue: nil)
var $mode: Binding<editmode?> { _mode.projectedValue }
var mode: EditMode? { _mode.wrappedValue }


…an `@Environment` looks like this:


// @Environment(\.editMode) var mode
var _mode: Environment<binding<editmode?>> = Environment(\.editMode)
// var $mode doesn't exist: Environment does not define a property named `projectedValue`
var mode: Binding<editmode?> { _mode.wrappedValue }


This double-wrapping means that you have to go through two layers, and the compiler is only helping you with the outer one. So to access the edit mode for real, you use `mode.wrappedValue`, like so:


var body: some View {
    if self.mode.wrappedValue?.isEditing ?? false {
        // editing
    }
    else {
        // not editing
    }
}

Thanks for this Jim. I tried this with the following:


struct ContentView: View {
    @Environment(\.editMode) var mode

    var body: some View {
        NavigationView {
            List {
                if self.mode?.wrappedValue.isEditing ?? true {
                    Text("Editing")
                }
                else {
                    Text("Not Editing")
                }
            }
            .navigationBarTitle(Text("Test Edit"))
            .navigationBarItems(trailing: EditButton())
        }
    }
}


Unfortunately this didn't work - it just displayed "Not Editing" all the time. Suspecting this had to do with the edit button and mode being declared at the same level I refactored it to the following, which does work.


struct ContentView: View {
    
    var body: some View {
        NavigationView {
            List {
                DisplayEditor()
            }
            .navigationBarTitle(Text("Test Edit"))
            .navigationBarItems(trailing: EditButton())
        }
    }
}

struct DisplayEditor: View {
    @Environment(\.editMode) var mode
    
    var body: some View {
        if self.mode?.wrappedValue.isEditing ?? true {
            return Text("Editing")
        }
        else {
            return Text("Not Editing")
        }
    }
}

That seems strange. I've built views that both contained an `EditButton` and used the editMode from the environment before and they were perfectly happy, i.e.


struct TodoList: View {
    @State var items: [TodoItem]
    @Environment(\.editMode) var editMode

    var body: some View {
        NavigationView {
            List(items) {
                // content...
            }
            .navigationBarItems(trailing: EditButton())
            .navigationBarTitle("To-Do Items")
        }
    }
}


This works fine for me in my tests. It makes sense, too—this is why `EnvironmentValues.editMode` is a `Binding`. The ultimate value is held elsewhere, and a binding to it is inserted at the root of the view tree, editable by anyone. If it were just a value rather than a binding, then the value would indeed only work downstream—it would have one value until some subview uses `.environment()` to provide another.


I wonder if it was failing only in the canvas? There's a problem with the canvas and edit modes because nothing actually inserts the binding into the environment when you're running a preview. To fix this I created a simple wrapper view that holds the edit mode and puts a binding to it in the environment:


@available(iOS 13.0, tvOS 13.0, *)
@available(OSX, unavailable)
@available(watchOS, unavailable)
struct EditModePreviewWrapper<Content: View>: View {
    @State var editMode: EditMode = .inactive
    var content: Content

    init(@ViewBuilder content: @escaping () -> Content) {
        self.content = content()
    }

    var body: some View {
        // Can't access @State outside of render loop (e.g. in preview creation)
        content.environment(\.editMode, $editMode)
    }
}

The code I posted was running in an app on the simulator. I tend not to use the canvas because of the problems with it. I agree that the edit button works fine applied to a list - the list contents display delete buttons as expected. What doesn't work, as shown in my example, is access to editMode in the same struct as its declaration.