12

I'd like to perform an action when the EditMode changes.

Specifically, in edit mode, the user can select some items to delete. He normally presses the trash button afterwards. But he may also press Done. When he later presses Edit again, the items that were selected previously are still selected. I would like all items to be cleared.

struct ContentView: View {
    @State var isEditMode: EditMode = .inactive
    @State var selection = Set<UUID>()
    var items = [Item(), Item(), Item(), Item(), Item()]

    var body: some View {
        NavigationView {
            List(selection: $selection) {
                ForEach(items) { item in
                    Text(item.title)
                }
            }
            .navigationBarTitle(Text("Demo"))
            .navigationBarItems(
                leading: EditButton(),
                trailing: addDelButton
            )
            .environment(\.editMode, self.$isEditMode)
        }
    }

    private var addDelButton: some View {
        if isEditMode == .inactive {
            return Button(action: reset) {
                Image(systemName: "plus")
            }
        } else {
            return Button(action: reset) {
                Image(systemName: "trash")
            }
        }
    }

    private func reset() {
        selection = Set<UUID>()
    }
}

Definition of Item:

struct Item: Identifiable {
    let id = UUID()
    let title: String

    static var i = 0
    init() {
        self.title = "\(Item.i)"
        Item.i += 1
    }
}
caram
  • 1,494
  • 13
  • 21
  • Already answered here: https://stackoverflow.com/a/57381570/3393964 – Casper Zandbergen Sep 04 '19 at 12:18
  • Possible duplicate of [SwiftUI Button as EditButton](https://stackoverflow.com/questions/57344305/swiftui-button-as-editbutton) – Casper Zandbergen Sep 04 '19 at 15:06
  • Cool, except now the bubbles to select elements no longer appear... So the real EditButton must be something different that just setting editMode to active. – caram Sep 05 '19 at 19:13
  • Plus, I also need `editMode` to be a `@State` variable so that the Trash button morphs back into a + when editMode is inactive. – caram Sep 05 '19 at 19:59

3 Answers3

19

UPDATED for iOS 15.

This solution catches 2 birds with one stone:

  1. The entire view redraws itself when editMode is toggle
  2. A specific action can be performed upon activation/inactivation of editMode

Hopes this helps someone else.

struct ContentView: View {
    @State var editMode: EditMode = .inactive
    @State var selection = Set<UUID>()
    @State var items = [Item(), Item(), Item()]

    var body: some View {
        NavigationView {
            List(selection: $selection) {
                ForEach(items) { item in
                    Text(item.title)
                }
            }
            .navigationTitle(Text("Demo"))
            .environment(\.editMode, self.$editMode)
            .toolbar {
                ToolbarItem(placement: .navigationBarLeading) {
                    editButton
                }
                ToolbarItem(placement: .navigationBarTrailing) {
                    addDelButton
                }
            }
        }
    }

    private var editButton: some View {
        Button(action: {
            self.editMode.toggle()
            self.selection = Set<UUID>()
        }) {
            Text(self.editMode.title)
        }
    }

    private var addDelButton: some View {
        if editMode == .inactive {
            return Button(action: addItem) {
                Image(systemName: "plus")
            }
        } else {
            return Button(action: deleteItems) {
                Image(systemName: "trash")
            }
        }
    }
    
    private func addItem() {
        items.append(Item())
    }
    
    private func deleteItems() {
        for id in selection {
            if let index = items.lastIndex(where: { $0.id == id }) {
                items.remove(at: index)
            }
        }
        selection = Set<UUID>()
    }
}
extension EditMode {
    var title: String {
        self == .active ? "Done" : "Edit"
    }
    
    mutating func toggle() {
        self = self == .active ? .inactive : .active
    }
}
caram
  • 1,494
  • 13
  • 21
  • 1
    FYI — One should be able to just declare the build in `EditButton()` as a trailing/leading `ToolbarItem`; this way we not only avoid `custom editButton` code, but also get support for full animation when switching modes. (_ToolbarItem because navigationBarItems is deprecated in iOS 13._) – Vinod Madigeri Feb 08 '22 at 17:22
  • I've update the code to use `.toolbar` instead of `.navigationBarItems`. – caram May 29 '22 at 17:21
8

I was trying forever, to clear List selections when the user exited editMode. For me, the cleanest way I've found to react to a change of editMode:

Make sure to reference the @Environment variable:

@Environment(\.editMode) var editMode

Add a computed property in the view to monitor the state:

private var isEditing: Bool {
   editMode?.wrappedValue.isEditing == true
}

Then use the .onChange(of:perform:) method:

.onChange(of: self.isEditing) { value in
  if value {
    // do something
  } else {
    // something else
  }
}

All together:

struct ContentView: View {
  
  @Environment(\.editMode) var editMode
  @State private var selections: [String] = []
  @State private var colors: ["Red", "Yellow", "Blue"]

  private var isEditing: Bool {
    if editMode?.wrappedValue.isEditing == true {
       return true
     }
     return false
  }
  
  var body: some View {

    List(selection: $selections) {
      ForEach(colors, id: \.self) { color in
        Text("Color")
      }
    }
    .toolbar {
      ToolbarItem(placement: .navigationBarTrailing) {
        EditButton()
      }
    }
    .onChange(of: isEditing) { value in
      if value == false {
        selection.removeAll()
      }
    }
  }

}
Peter Kreinz
  • 7,979
  • 1
  • 64
  • 49
Trent M.
  • 101
  • 2
  • 4
0

In case someone want to use SwiftUI's EditButton() instead of custom a Button and still want to perform action when isEditing status changes

You can use View extension

extension View {
    func onChangeEditMode(editMode: EditMode?, perform: @escaping (EditMode?)->()) -> some View {
        ZStack {
            Text(String(describing: editMode))
                .opacity(0)
                .onChange(of: editMode, perform: perform)
            self
        }
    }
}

Then you can use it like this

struct TestEditModeView: View {
    @Environment(\.editMode) var editMode
    @State private var editModeDescription: String = "nil"
    var body: some View {
        VStack {
            Text(editModeDescription)
            EditButton()
        }
        .onChangeEditMode(editMode: editMode?.wrappedValue) {
            editModeDescription = String(describing: $0)
        }
    }
}
meomeomeo
  • 764
  • 4
  • 7