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
}
}
}
}
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 View
s and/or ViewModifier
s, 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.