SwiftUI Form with Multiple EditButtons

892 Views Asked by At

Trying to have a Form with multiple sections and each Section with it's own EditButton.

  1. How to trigger a Section into "edit mode" without triggering all sections in the Form, as seen in the attached gif.

  2. How to track if the EditButton in a certain Section is triggered so that a Button appears in that Section.

I used code from these two sources: developer.apple.com, stackoverflow.com

enter image description here

Here is the code:

import SwiftUI

struct ContentView: View {
    @Environment(\.editMode) private var editMode
    @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
    
    var body: some View {
        Form {
            // Section 1
            Section (header:
                        EditButton().frame(maxWidth: .infinity, alignment: .trailing)
                        .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)
                
                // Add item option
                if editMode?.wrappedValue.isEditing ?? true /*isEditingSection1*/ {
                    Button ("Add Item") {
                        // add action
                    }
                }
            }
            
            // Section 2
            Section (header:
                        EditButton().frame(maxWidth: .infinity, alignment: .trailing)
                        .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: deleteSection2)
                .onMove(perform: moveSection2)
                
                // Add item option
                if editMode?.wrappedValue.isEditing ?? true /*isEditingSection2*/ {
                    Button ("Add Item") {
                        // add action
                    }
                }
            }
            
        }
    }
    
    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)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
2

There are 2 best solutions below

1
On BEST ANSWER

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

enter image description here

0
On

Update for iOS 16:

I wish I knew the why, but in iOS 16 you have to phrase the boolean expression like this.

.deleteDisabled(isEditingSection2)
.deleteDisabled((isEditingSection2 || isEditingSection3))
// Etc...

In iOS 15.5 you can get away with either.

.deleteDisabled(!isEditingSection1)
// Or...
.deleteDisabled(isEditingSection2)

Here's a complete ContentView and EditButton showing this. I added a third section to help demo how the boolean OR logic should be applied inside .deleteDisabled and .moveDisabled.

import SwiftUI

struct ContentView: View {
    @State private var section1: [String] = ["Item 1", "Item 2"]
    @State private var section2: [String] = ["Item 3", "Item 4"]
    @State private var section3: [String] = ["Item 5", "Item 6"]
    @State private var isEditingSection1 = false
    @State private var isEditingSection2 = false
    @State private var isEditingSection3 = false
    
    private var isEditingOn: Bool { //<=== Here
        isEditingSection1 || isEditingSection2 || isEditingSection3
    }
    
    var body: some View {
        Form {
            // Section 1
            Section (header:
                EditButton(isEditing: $isEditingSection1, printBools: printBools)
                .frame(maxWidth: .infinity, alignment: .trailing)
                .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(isEditingSection2 || isEditingSection3) //<=== Here
                        .deleteDisabled((isEditingSection2 || isEditingSection3)) //<=== Here
                        
                        // BIG NOTE!!
                        // This works in iOS 15.5, but not iOS 16:  `.deleteDisabled(!isEditingSection1)`
                        // This works in both 15.5 and 16.0.  Why??? `.deleteDisabled(isEditingSection2 || isEditingSection3)`
                        
                        // Add item option
                        if isEditingSection1 { //<=== Here
                            Button ("Add Item") {
                                // add action
                            }
                        }
                    }
            
            // Section 2
            Section(header:
                EditButton(isEditing: $isEditingSection2, printBools: printBools)
                .frame(maxWidth: .infinity, alignment: .trailing)
                .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: deleteSection2)
                        .onMove(perform: moveSection2)
                        .moveDisabled(isEditingSection1 || isEditingSection3) //<=== Here
                        .deleteDisabled(isEditingSection1 || isEditingSection3) //<=== Here
                        
                        // Add item option
                        if isEditingSection2 { //<=== Here
                            Button ("Add Item") {
                                // add action
                            }
                        }
                    }
            
            // Section 3
            Section(header:
                EditButton(isEditing: $isEditingSection3, printBools: printBools)
                .frame(maxWidth: .infinity, alignment: .trailing)
                .overlay(
                    HStack {
                        Image(systemName: "tray")
                            .foregroundColor(Color.gray)
                        Text("Section 3")
                            .textCase(.none)
                            .foregroundColor(Color.gray)
                    }, alignment: .leading)
                    .foregroundColor(.blue)) {
                        ForEach(section3, id: \.self) { item in
                            Text(item)
                        }
                        .onDelete(perform: deleteSection3)
                        .onMove(perform: moveSection3)
                        .moveDisabled(isEditingSection1 || isEditingSection2) //<=== Here
                        .deleteDisabled(isEditingSection1 || isEditingSection2) //<=== Here
                        
                        // Add item option
                        if isEditingSection3 { //<=== 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)
    }
    
    func deleteSection3(at offsets: IndexSet) {
        section3.remove(atOffsets: offsets)
    }
    
    func moveSection3(from source: IndexSet, to destination: Int) {
        section3.move(fromOffsets: source, toOffset: destination)
    }
    
    
    func printBools() {
//        let _ = print("isEditingSection1 = \(isEditingSection1)")
//        let _ = print("isEditingSection2 = \(isEditingSection2)")
    }
}


struct EditButton: View {
    @Binding var isEditing: Bool
    
    let printBools: () -> Void

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


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Note that logically here, both expressions !isEditingSection1 and isEditingSection2 || isEditingSection3 are equivalent when we consider how we are using them here.

isEditingSection1 ! isEditingSection1 isEditingSection2 OR isEditingSection3
true false false
false true true, if editing another section. Otherwise false.