Swiftui Editbutton Action on Done

SwiftUI EditButton action on Done

From what I understand, the EditButton is meant to put the entire environment in edit mode. That means going into a mode where, for example, you rearrange or delete items in a list. Or where text fields become editable. Those sorts of things. "Done" means "I'm done being in edit mode", not "I want to apply my changes." You would want a different "Apply changes," "Save," "Confirm" or whatever button to do those things.

SwiftUI how to perform action when EditMode changes?

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
}
}

SwiftUI: EditButton says Done after swipe to delete action

It seems EditButton is incompatible with this new way of using @StateObject to declare the store object. I had the idea to try reverting to the old way of creating the store object and surprisingly it fixed the issue. Change the ScrumStore to this

class ScrumStore: ObservableObject {
@Published var scrums: [DailyScrum] = []

static var shared = ScrumStore()

And change the ScrumdingerApp to this:

struct ScrumdingerApp: App {
private var store = ScrumStore.shared

var body: some Scene {
WindowGroup {
NavigationView {
ScrumsView(store: store) {

And ScrumsView to this:

struct ScrumsView: View {
//@Binding var scrums: [DailyScrum]
@ObservedObject var store: ScrumStore

...

var body: some View {
List {
ForEach($store.scrums) { $scrum in

And change the other scrums to store.scrums too.

This makes the edit button work properly for swipe to delete

I would also improve the code by making the store object be an Environment object, e.g.

struct ScrumdingerApp: App {
private var store = ScrumStore.shared

var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(store)
}
}

struct ContentView: View {
@EnvironmentObject var store: ScrumStore

...

I would also improve how the model structs are loaded and saved, e.g. I would load it in the object's init and have a function to save it. The function could be called on a scene phase or after the model is mutated. Before doing too much loading and saving you might want to try FileDocument which does all that for you and gives you iCloud drive support for free. I recommend the document app WWDC videos to learn that, there is a new one every year.

SwiftUI Button as EditButton

The implementation below replaces EditButton's functionality with a Button:

import SwiftUI

struct ContentView: View {

@State var isEditing = false
@State var selection = Set<String>()

var names = ["Karl", "Hans", "Faustao"]

var body: some View {
NavigationView {
VStack {
List(names, id: \.self, selection: $selection) { name in
Text(name)
}
.navigationBarTitle("Names")
.environment(\.editMode, .constant(self.isEditing ? EditMode.active : EditMode.inactive)).animation(Animation.spring())
Button(action: {
self.isEditing.toggle()
}) {
Text(isEditing ? "Done" : "Edit")
.frame(width: 80, height: 40)
}
.background(Color.yellow)
}
.padding(.bottom)
}
}
}

#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif

Result

result

However, by doing so, selection handling needs to be implemented by our own (which may or may not be an issue).

Unfortunately there isn't much documentation around that at this point:
https://developer.apple.com/documentation/swiftui/list/3367016-init

SwiftUI detect edit mode

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

SwiftUI EditButton in HStack not activating edit mode

Here is working solution - looks like they require that EditButton was a root view of section, so we can construct everything else above it. (tested with Xcode 11.4 / iOS 13.4)

Note: @Environment(\.editMode) var editMode is not needed

demo

Section(header:
EditButton().frame(maxWidth: .infinity, alignment: .trailing)
.overlay(Text("Header"), alignment: .leading)
)
{
ForEach(items, id: \.self) { item in
Text(item)
}
.onMove(perform: reorderItems)
.onDelete(perform: deleteItems)
}

How to check EditButton/EditMode state in SwiftUI

You can achieve this by setting .environment with @State mode.

struct TestView: View {
@State private var list = [1,2,3,4,5]
@State private var selection = Set<Int>()
@State var mode: EditMode = .inactive //< -- Here

var body: some View {
NavigationView {
List(selection: $selection) {
ForEach(list, id: \.self) { item in
Text("\(item)")
}
}

.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
EditButton()
}
ToolbarItem(placement: .navigationBarTrailing) {
Text(mode == .active ? "Editing" : "Not Editing")
}
}.environment(\.editMode, $mode) //< -- Here
}
}
}

SwiftUI Form with Multiple EditButtons

There is no inbuilt thing for set different editing mode for each section.

But you can use it explicitly to set editing mode and disable/enable delete and move action for each row.

Here is the possible solution demo.

For this, you need to first create your own EditButton with a binding bool value.

struct EditButton: View {
@Binding var isEditing: Bool

var body: some View {
Button(isEditing ? "DONE" : "EDIT") {
withAnimation {
isEditing.toggle()
}
}
}
}

Now your Form view is.

struct ContentViewEditModeDemo: View {
@State private var section1: [String] = ["Item 1", "Item 2"]
@State private var section2: [String] = ["Item 3", "Item 4"]
@State private var isEditingSection1 = false
@State private var isEditingSection2 = false

private var isEditingOn: Bool { //<=== Here
isEditingSection1 || isEditingSection2
}

var body: some View {
Form {
// Section 1
Section (header:
EditButton(isEditing: $isEditingSection1).frame(maxWidth: .infinity, alignment: .trailing) //<=== Here
.overlay(
HStack {
Image(systemName: "folder")
.foregroundColor(Color.gray)
Text("Section 1")
.textCase(.none)
.foregroundColor(Color.gray)
}, alignment: .leading)
.foregroundColor(.blue)) {
ForEach(section1, id: \.self) { item in
Text(item)
}
.onDelete(perform: deleteSection1)
.onMove(perform: moveSection1)
.moveDisabled(!isEditingSection1) //<=== Here
.deleteDisabled(!isEditingSection1) //<=== Here

// Add item option
if isEditingSection1 { //<=== Here
Button ("Add Item") {
// add action
}
}
}

// Section 2
Section(header:
EditButton(isEditing: $isEditingSection2).frame(maxWidth: .infinity, alignment: .trailing) //<=== Here
.overlay(
HStack {
Image(systemName: "tray")
.foregroundColor(Color.gray)
Text("Section 2")
.textCase(.none)
.foregroundColor(Color.gray)
}, alignment: .leading)
.foregroundColor(.blue)) {
ForEach(section2, id: \.self) { item in
Text(item)
}
.onDelete(perform: deleteSection1)
.onMove(perform: moveSection1)
.moveDisabled(!isEditingSection2) //<=== Here
.deleteDisabled(!isEditingSection2) //<=== Here

// Add item option
if isEditingSection2 { //<=== Here
Button ("Add Item") {
// add action
}
}
}
}.environment(\.editMode, isEditingOn ? .constant(.active) : .constant(.inactive)) //<=== Here
}

func deleteSection1(at offsets: IndexSet) {
section1.remove(atOffsets: offsets)
}

func moveSection1(from source: IndexSet, to destination: Int) {
section1.move(fromOffsets: source, toOffset: destination)
}

func deleteSection2(at offsets: IndexSet) {
section2.remove(atOffsets: offsets)
}

func moveSection2(from source: IndexSet, to destination: Int) {
section2.move(fromOffsets: source, toOffset: destination)
}
}

Sample Image



Related Topics



Leave a reply



Submit