Home > Software engineering >  SwiftUI Limit Scope of EditButton() by Platform
SwiftUI Limit Scope of EditButton() by Platform

Time:11-18

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

CodePudding user response:

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

CodePudding user response:

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()
    }
}
  • Related