swiftlistswiftuieditmode

why is change in editingMode not propogating to other classes that declare it?


I have a class TitleList that allows the user to reorder its rows via swipe action. Once they're done reordering, they can tap anywhere on the UI (outside of the list ordering icons) to disable editingMode. In certain instances, I want the class that contains the TitleList to be able to disable editingMode (like if the user taps a button to launch a .sheet).

However, when I declare @Environment(\.editMode) private var editMode in the containing class and try to do self.editMode?.wrappedValue = .inactive after a button is pushed, the change does not propogate to TitleList and editing mode is not disabled. Specifically, nothing happens in TitleList's

.onChange(of: self.editMode?.wrappedValue) { oldValue, newValue in
    if newValue == .inactive {
        self.isReordering = false
    }
}

Does anyone know why this is not working? Is it a bug? Here is the full code:

import SwiftUI

struct HomeView: View {
    @Environment(\.editMode) private var editMode
    @State private var sheetIsPresented = false
    @State private var titles: [String] = ["Title 1", "Title 2", "Title 3"]

    var body: some View {
        NavigationStack {
            VStack {
                List {
                    TitleList(titles: $titles)
                }
                .toolbar {
                    ToolbarItem(placement: .navigationBarTrailing) {
                        Button {
                            editMode?.wrappedValue = .inactive
                        } label: {
                            Label("Plus", systemImage: "plus")
                        }
                    }
                }
            }
        }
        .onChange(of: editMode?.wrappedValue) { _, _ in
            print("Worked")
        }
    }
}

struct TitleList: View {
    @Binding var titles: [String]
    @Environment(\.editMode) private var editMode
    @State private var isReordering = false

    var body: some View {
        ForEach(titles, id: \.self) { title in
            NavigationLink(destination: Text("Test")) {
                Text(title)
            }
            .moveDisabled(!isReordering)
            .swipeActions(allowsFullSwipe: false) {
                Button {
                    editMode?.wrappedValue = .active
                    isReordering = true
                } label: {
                    Label("Reorder", systemImage: "list.bullet")
                }
            }
            .contentShape(Rectangle())
            .highPriorityGesture(
                TapGesture().onEnded {
                    if isReordering {
                        withAnimation {
                            editMode?.wrappedValue = .inactive
                        }
                    }
                },
                isEnabled: isReordering
            )
        }
        .onMove { source, destination in
            titles.move(fromOffsets: source, toOffset: destination)
        }
        .contentShape(Rectangle())
        .onChange(of: editMode?.wrappedValue) { _, newValue in
            print("Test")
            if newValue == .inactive {
                isReordering = false
            }
        }
    }
}

Solution

  • I'm not sure if this particular case is a bug, but editMode is definitely broken in one way or another. The example code in its documentation doesn't actually work as advertised, at least.

    It's not that the change in HomeView.editMode is not propagated to TitleList - it's that HomeView.editMode doesn't have any effect at all. The plus button isn't even changing HomeView.editMode.

    It seems like editMode only works if you get it from an @Environment within an editable view (e.g. List) or a toolbar. You can wrap your existing code into Views and/or ViewModifiers, and put them in those places.

    struct EditModeChanger: View {
        @Environment(\.editMode) var editMode
        var body: some View {
            Button {
                editMode?.wrappedValue = .inactive
            } label: {
                Label("Plus", systemImage: "plus")
            }
        }
    }
    
    struct EditModeDetector: ViewModifier {
        @Environment(\.editMode) var editMode
        func body(content: Content) -> some View {
            content.onChange(of: editMode?.wrappedValue) { _, _ in
                print("Worked")
            }
        }
    }
    
    List {
        TitleList(titles: $titles)
            .modifier(EditModeDetector())
    }
    .toolbar {
        ToolbarItem(placement: .navigationBarTrailing) {
            EditModeChanger()
        }
    }
    

    Now the plus button works as expected, and "Worked" is also being printed.