5

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 Answers 2

3

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.
Sign up to request clarification or add additional context in comments.

Comments

1

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

1 Comment

Does not work with iOS 16.0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.