skip to Main Content

I have the following situation: in SwiftUI you can use List‘s onMove and onDelete to reorder and delete items of an array. Swift even provides an EditButton which will force the environment editMode so you can move and delete items. There are 2 ways for you to move items, while having onMove on onDelete:

  1. Hold to move and swipe to delete
  2. Tap on edit mode and use the red dot to delete or the triple lines to drag and reorder.

I want to be able to use only number 2 while number 1 being deactivated so I tried the following:

struct CustomListData: Identifiable {
    let id = UUID()
    let title: String
}

struct ContentView: View {
    @State var customListData = [
        CustomListData(title: "One"),
        CustomListData(title: "Two"),
        CustomListData(title: "Three"),
        CustomListData(title: "Four"),
        CustomListData(title: "Five")
    ]
    @Environment(.editMode) var editMode

    var inEditMode: Bool {
        editMode?.wrappedValue.isEditing ?? false
    }

    var body: some View {
        VStack {
            List {
                ForEach(customListData) { item in
                    Text(item.title)
                }
                .onDelete(perform: inEditMode ? deleteItems : nil)
                .onMove(perform: inEditMode ? moveItems : nil)
            }

            EditButton()
        }
    }

    func deleteItems(at offsets: IndexSet) {
        customListData.remove(atOffsets: offsets)
    }

    func moveItems(fromIndex: IndexSet, newIndex: Int) {
        customListData.move(fromOffsets: fromIndex, toOffset: newIndex)
    }
}

The problem is that when edit button is tapped I don’t go into edit mode aka not seeing the red deletion circle on the left and triple lines on the right. Instead I am just able to move them in behaviour 1. For more context this is what I mean https://imgur.com/ZsGFgJ8

2

Answers


  1. Unfortunately, as of iOS 17, SwiftUI’s EditButton is still buggy and is nowhere near a parity of the UIKit’s UITableView delegate methods functionalities.

    In terms of your question, it is partially due to the buggy editing behaviors. Consider the following situations:

    1. Change the modifiers to below that doesn’t use edit mode at all:
    .onDelete(perform: deleteItems)
    .onMove(perform: moveItems)
    

    Then both 1 and 2 would work, which is how SwiftUI expects you to use the EditButton.

    1. Use your current version. When the edit button is tapped, try move or delete a row. You’ll see the buggy behavior that when you move a row, the row will show both indicators!

    2. Because of the buggy behavior of EditButton, there isn’t an elegant way to make it work as you described 😩. There are 2 alternatives that are worth considering:

      1. (not recommended) Mess up the view’s identity. Add .id(inEditMode) to each list row or the entire list, to force the view to recompute when the edit mode changes.
      2. (what we did) instead of using EditButton, create custom UIs to mimic the buttons in the row. Therefore, you have full control over the editing states.
    Login or Signup to reply.
  2. The undocumented trick with @Environment(.editMode) is it needs to be in a custom View that is inside a container like List, Form, VStack etc. or inside a modifier applied to one of those containers like .toolbar. The reason is it is the List that sets the editMode binding in the @Environment. So if you try to access that in a View higher in the hierarchy then there is no valid editMode binding which is why attempting to read its wrapped value is always false. Restructuring it to something like this should fix the problem:

    struct ContentView: View {
        var body: some View {
            NavigationStack {
                List { // sets the Environment(.editMode)
                    CustomList() // can use editMode
                }
                .toolbar {
                    ToolbarItemGroup {
                        EditButton() // can use editMode
                    }
                }
                // EditButton() it cannot use editMode here
            }
        }
    }
    
    struct CustomList: View {
    
        @State var customListData = [
            CustomListData(title: "One"),
            CustomListData(title: "Two"),
            CustomListData(title: "Three"),
            CustomListData(title: "Four"),
            CustomListData(title: "Five")
        ]
    
        @Environment(.editMode) var editMode
    
        var inEditMode: Bool {
            editMode?.wrappedValue.isEditing ?? false
        }
    
        var body: some View {
            ForEach(customListData) { item in
                Text(item.title)
            }
            .onDelete(perform: inEditMode ? deleteItems : nil)
            .onMove(perform: inEditMode ? moveItems : nil)
        }
    
        func deleteItems(at offsets: IndexSet) {
            customListData.remove(atOffsets: offsets)
        }
    
        func moveItems(fromIndex: IndexSet, newIndex: Int) {
            customListData.move(fromOffsets: fromIndex, toOffset: newIndex)
        }
    }
    
    struct CustomListData: Identifiable {
        let id = UUID()
        var title: String
    }
    
    
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search