skip to Main Content

I am building an app for iOS and Mac Catalyst and have been able to code most of the
experience that I want except for functions that use swipe to delete in iOS.

The view includes multiple sections, each with a List and ForEach closure. I want to
be able to add the EditButton() function to the header of each section and have it
apply only to that section’s List.

I can
add an EditButton() function to gain this functionality, however,
so far I have only been able to make that work for the entire screen, not for the
individual sections.

I have tried refactoring the code for each section into functions and into structs
(as shown below). In all cases the EditButton() activates the delete icons for ALL
list rows, not just the section with the button.

enter image description here

I have also tried placing the EditButton() inside the section in the VStack. No difference.

Here’s a simple example with the latest code attempt:

struct ContentView: View {

    @State private var selectedItem: String?
    @State private var items = ["One", "Two", "Three", "Four", "Five"]
    @State private var fruits = ["Apple", "Orange", "Pear", "Lemon", "Grape"]

    var body: some View {
        NavigationSplitView {
            ItemSection(selectedItem: $selectedItem, items: $items)
            FruitSection(selectedItem: $selectedItem, fruits: $fruits)
        } detail: {
            if let selectedItem {
                ItemDetailView(selectedItem: selectedItem)
            } else {
                EmptyView()
            }
        }//nav
    }//body

}//struct

struct ItemSection: View {

    @Binding var selectedItem: String?
    @Binding var items: [String]

    var body: some View {
        Section {
            VStack {
                List(selection: $selectedItem) {
                    ForEach(items, id: .self) { item in
                        NavigationLink(value: item) {
                            Text(item)
                        }
                    }
                    .onDelete { items.remove(atOffsets: $0) }
                }
                .listStyle(PlainListStyle())
            }//v
            .padding()
        } header: {
            HStack {
                Text("Section for Items")
                Spacer()
                //uncomment when you have it working
                //#if targetEnvironment(macCatalyst)
                EditButton()
                //#endif
            }//h
            .padding(.horizontal, 10)
        }//section and header
    }//body
}//item section

struct FruitSection: View {

    @Binding var selectedItem: String?
    @Binding var fruits: [String]

    var body: some View {
        Section {
            VStack {
                List(selection: $selectedItem) {
                    ForEach(fruits, id: .self) { fruit in
                        NavigationLink(value: fruit) {
                            Text(fruit)
                        }
                    }
                    .onDelete { fruits.remove(atOffsets: $0) }
                }
                .listStyle(PlainListStyle())
            }//v
            .padding()
        } header: {
            HStack {
                Text("Section for Fruits")
                Spacer()
            }//h
            .padding(.horizontal, 10)
        }//section fruit
    
    }//body
}//fruit section

struct ItemDetailView: View {
    var selectedItem: String

    var body: some View {
        VStack {
            Text(selectedItem)
            Text("This is the DetailView")
        }
    }
}

Any guidance would be appreciated. Xcode 14.0.1 iOS 16

3

Answers


  1. import SwiftUI
    struct ContentView: View {
    
        @State private var selectedItem: String?
        @State private var items = ["One", "Two", "Three", "Four", "Five"]
        @State private var fruits = ["Apple", "Orange", "Pear", "Lemon", "Grape"]
    
        var body: some View {
            NavigationSplitView {
                ItemSection(selectedItem: $selectedItem, items: $items)
                FruitSection(selectedItem: $selectedItem, fruits: $fruits)
            } detail: {
                if let selectedItem {
                    ItemDetailView(selectedItem: selectedItem)
                } else {
                    EmptyView()
                }
            }//nav
        }//body
    
    }//struct
    
    struct ItemSection: View {
    
        @Binding var selectedItem: String?
        @Binding var items: [String]
        @State var isEditMode = false // this is what you need
        var body: some View {
            Section {
                VStack {
                    List(selection: $selectedItem) {
                        ForEach(items, id: .self) { item in
                            NavigationLink(value: item) {
                                Text(item)
                            }
                        }
                        .onDelete { items.remove(atOffsets: $0) }
                    }
                    .environment(.editMode, isEditMode ? .constant(.active) : .constant(.inactive)) // and set this
                    .listStyle(PlainListStyle())
                }//v
                .padding()
            } header: {
                HStack {
                    Text("Section for Items")
                    Spacer()
                    //uncomment when you have it working
                    //#if targetEnvironment(macCatalyst)
                    Button { // you also need to set EditButton() -> Button()
                        withAnimation {
                            isEditMode.toggle()
                        }
                    } label: {
                        Text(isEditMode ? "Done" : "Edit")
                    }
                    //#endif
                }//h
                .padding(.horizontal, 10)
            }//section and header
        }//body
    }//item section
    
    struct FruitSection: View {
    
        @Binding var selectedItem: String?
        @Binding var fruits: [String]
        @State var isEditMode = false // same as this section
        var body: some View {
            Section {
                VStack {
                    List(selection: $selectedItem) {
                        ForEach(fruits, id: .self) { fruit in
                            NavigationLink(value: fruit) {
                                Text(fruit)
                            }
                        }
                        .onDelete { fruits.remove(atOffsets: $0) }
                    }
                    .environment(.editMode, isEditMode ? .constant(.active) : .constant(.inactive))
                    .listStyle(PlainListStyle())
                }//v
                .padding()
            } header: {
                HStack {
                    Text("Section for Fruits")
                    Spacer()
                    Button {
                        withAnimation {
                            isEditMode.toggle()
                        }
                    } label: {
                        Text(isEditMode ? "Done" : "Edit")
                    }
    
                }//h
                .padding(.horizontal, 10)
            }//section fruit
        
        }//body
    }//fruit section
    
    struct ItemDetailView: View {
        var selectedItem: String
    
        var body: some View {
            VStack {
                Text(selectedItem)
                Text("This is the DetailView")
            }
        }
    }
    
    Login or Signup to reply.
  2. Here’s a more general & simplified approach using PreferenceKey:

    struct EditModeViewModifier: ViewModifier {
        var forceEditing: Bool?
        @State var isEditing = false
        func body(content: Content) -> some View {
            content
                .onPreferenceChange(IsEditingPrefrenceKey.self) { newValue in
                    withAnimation {
                        isEditing = newValue
                    }
                }.environment(.editMode, .constant((forceEditing ?? isEditing) ? .active: .inactive))
        }
    }
    
    extension View {
        func editMode(_ editing: Bool? = nil) -> some View {
            modifier(EditModeViewModifier(forceEditing: editing))
        }
    }
    
    struct EditingButton: View {
        @State var isEditing = false
        var body: some View {
            Button(action: {
                isEditing.toggle()
            }) {
                Text(isEditing ? "Done" : "Edit")
            }.preference(key: IsEditingPrefrenceKey.self, value: isEditing)
        }
    }
    
    struct IsEditingPrefrenceKey: PreferenceKey {
        static var defaultValue = false
        static func reduce(value: inout Bool, nextValue: () -> Bool) {
            value = nextValue()
        }
    }
    

    You use EditingButton instead of EditButton, & use .editMode() at then end of your View. Then your sections become something like this:

    struct ItemSection: View {
        @Binding var selectedItem: String?
        @Binding var items: [String]
        var body: some View {
            Section {
                VStack {
                    List(selection: $selectedItem) {
                        ForEach(items, id: .self) { item in
                            NavigationLink(value: item) {
                                Text(item)
                            }
                        }.onDelete { items.remove(atOffsets: $0) }
                    }.listStyle(PlainListStyle())
                }.padding()
            } header: {
                HStack {
                    Text("Section for Items")
                    Spacer()
                    //uncomment when you have it working
                    //#if targetEnvironment(macCatalyst)
                    EditingButton()
                    //#endif
                }.padding(.horizontal, 10)
            }.editMode()
        }
    }
    
    struct FruitSection: View {
        @Binding var selectedItem: String?
        @Binding var fruits: [String]
        var body: some View {
            Section {
                VStack {
                    List(selection: $selectedItem) {
                        ForEach(fruits, id: .self) { fruit in
                            NavigationLink(value: fruit) {
                                Text(fruit)
                            }
                        }.onDelete { fruits.remove(atOffsets: $0) }
                    }.listStyle(PlainListStyle())
                }.padding()
            } header: {
                HStack {
                    Text("Section for Fruits")
                    Spacer()
                    EditingButton()
                }.padding(.horizontal, 10)
            }.editMode()
        }
    }
    
    Login or Signup to reply.
  3. A more concise version of Timmy’s answer uses this reusable code:

    import SwiftUI
    
    struct EditingButton: View {
      @Binding var isEditing: Bool
    
      var body: some View {
          Button(isEditing ? "Done" : "Edit", action: changeEditing)
          .preference(key: IsEditingPrefrenceKey.self, value: isEditing)
    }
    
    func changeEditing() { withAnimation { isEditing.toggle() } }
    
    struct IsEditingPrefrenceKey: PreferenceKey {
        static var defaultValue = false
        static func reduce(value: inout Bool, nextValue: () -> Bool) { value = nextValue() }
    }
    
    }// EditingButton
    
    
    extension View {
        func editMode(_ editing: Bool) -> some View {
            environment(.editMode, editing ? .constant(.active) : .constant(.inactive))
        }
    }
    

    Then the ItemSection is like this:

    struct ItemSection: View {
    
    @Binding var selectedItem: String?
    @Binding var items: [String]
    @State var isEditMode = false // this is what you need
    var body: some View {
        Section {
            VStack {
                List(selection: $selectedItem) {
                    ForEach(items, id: .self) { item in
                        NavigationLink(value: item) {
                            Text(item)
                        }
                    }
                    .onDelete { items.remove(atOffsets: $0) }
                    .onMove(perform: move)
                }
                .editMode(isEditMode)
                .listStyle(PlainListStyle())
            }//v
            .padding()
        } header: {
            HStack {
                Text("Section for Items")
                Spacer()
                EditingButton(isEditing: $isEditMode)
            }//h
            .padding(.horizontal, 10)
        }//section and header
    }// body
    
    private func move(indexes: IndexSet, dest: Int) {
        print("Move item from indexset (indexSetList(indexes)) to index (dest)")
        items.move(fromOffsets: indexes, toOffset: dest)
    }// move
    
    }//item section
    
    func indexSetList(_ indexes: IndexSet) -> String {
        guard !indexes.isEmpty else { return "none"}
        return Array(indexes).map(String.init).joined(separator: " ")
    }
    

    I have allowed drag and drop reordering to make it do what I was interested in as well.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search