SwiftUI detect edit mode

Viewed 219

I've returned to iOS development after a while and I'm rebuilding my Objective-C app from scratch in SwiftUI.

One of the things I want to do is use the default Edit Mode to allow entries in a List (backed by Core Data on CloudKit) to switch between a NavigationLink to a detail view and an edit view.

The main approach seems to be to handle it through a if statement that detects edit mode. The Apple documentation provides the following snippet for this approach on this developer page: https://developer.apple.com/documentation/swiftui/editmode

@Environment(\.editMode) private var editMode
@State private var name = "Maria Ruiz"

var body: some View {
    Form {
        if editMode?.wrappedValue.isEditing == true {
            TextField("Name", text: $name)
        } else {
            Text(name)
        }
    }
    .animation(nil, value: editMode?.wrappedValue)
    .toolbar { // Assumes embedding this view in a NavigationView.
        EditButton()
    }
}

However, this does not work (I've embedded the snippet in a NavigationView as assumed). Is this a bug in Xcode 13.4.1? iOS 15.5? Or am I doing something wrong?

Update1:

Based on Asperi's answer I came up with the following generic view to handle my situation:

import SwiftUI

struct EditableRow: View {
#if os(iOS)
    @Environment(\.editMode) private var editMode
#endif
    
    @State var rowView: AnyView
    @State var detailView: AnyView
    @State var editView: AnyView

    var body: some View {
        NavigationLink{
            if(editMode?.wrappedValue.isEditing == true){
                editView
            }
            else{
                detailView
            }
            
        }label: {
            rowView
        }
    }
}

struct EditableRow_Previews: PreviewProvider {
    static var previews: some View {
        NavigationView {
            VStack {
                EditButton()
                EditableRow(rowView: AnyView(Text("Row")), detailView: AnyView(Text("Detail")), editView: AnyView(Text("Edit")))
            }
        }
    }

The preview works as expected, but this works partially in my real app. When I implement this the NavigationLink works when not in Edit Mode, but doesn't do anything when in Edit Mode. I also tried putting the whole NavigationLink in the if statement but that had the same result. Any idea why this isn't working?

Update2:

Something happens when it's inside a List. When I change the preview to this is shows the behavior I'm getting:

struct EditableRow_Previews: PreviewProvider {
    static var previews: some View {
        NavigationView {
            List {
                EditableRow(rowView: AnyView(GroupRow(title: "Title", subTitle: "Subtitle", type: GroupType.personal)), detailView: AnyView(EntryList()), editView: AnyView(Text("Edit")))
            }
            .navigationBarItems(trailing:
                HStack{
#if os(iOS)
                    EditButton()
#endif
                }
            )
         }
    }
}

Update3:

Found this answer: SwiftUI - EditMode and PresentationMode Environment

This claims the default EditButton is broken, which seems to be true. Replacing the default button with a custom one works (be sure to add a withAnimation{} block to get all the behavior from the stock button. But it still doesn't work for my NavigationLink...

Update4:

Ok, tried passing an "isEditing" Bool to the above View, not to depend on the Environment variable being available. This works as long as the View (a ForEach within a List in my case) isn't in "Editing Mode" whatever happens at that point breaks any NavigationLink it seems.

Update5:

Basically my conclusion is that the default Edit Mode is meant to edit the "List Object" as a whole enabling moving and deleting of rows. In this mode Apple feels that editing the rows themselves isn't something you'd want to do. I can see this perspective. If, however, you still want to enable a NavigationLink from a row in Edit Mode, this answer should help: How to make SwiftUI NavigationLink work in edit mode?

Asperi's answer does cover why the detection doesn't work. I did find that Edit Mode detection does work better when setting the edit mode manually and not using the default EditButton, see the answer above for details.

2 Answers

It is on same level so environment is not visible, because it is activated for sub-views.

A possible solution is to separate dependent part into standalone view, like

    Form {
        InternalView()
    }
    .toolbar {
        EditButton()
    }

Tested with Xcode 13.4 / iOS 15.5

demo

Test module on GitHub

@Asperi's answer worked well for me. However I wanted to still be able to access the editMode in the same hierarchy. As a workaround I created the following:

Example

Usage

struct ContentView: View {
    @State
    private var editMode: EditMode = .inactive

    var body: some View {
        NavigationView {
            Form {
                if editMode.isEditing == true {
                    Color.red
                } else {
                    Color.blue
                }
            }
            .editModeFix($editMode)
            .toolbar {
                EditButton()
            }
        }
    }
}

Implementation

extension View {
    func editModeFix(_ editMode: Binding<EditMode>) -> some View {
        modifier(EditModeFixViewModifier(editMode: editMode))
    }
}

private struct EditModeFixView: View {
    @Environment(\.editMode)
    private var editModeEnvironment
    
    @Binding
    var editMode: EditMode
    
    var body: some View {
        Color.clear
            .onChange(of: editModeEnvironment?.wrappedValue) { editModeEnvironment in
                if let editModeEnvironment = editModeEnvironment {
                    editMode = editModeEnvironment
                }
            }
            .onChange(of: editMode) {
                editModeEnvironment?.wrappedValue = $0
            }
    }
}

private struct EditModeFixViewModifier: ViewModifier {
    @Binding
    var editMode: EditMode
    
    func body(content: Content) -> some View {
        content
            .overlay {
                EditModeFixView(editMode: $editMode)
            }
    }
}
Related