SwiftUI Modal Badness

I’ve had a longstanding issue in Retrospective Timelines. I have a simple EditButton on the list of timelines in my app. This button toggles the list into edit mode where the user can perform several actions.

  1. Reorder timelines
  2. Delete a timeline
  3. Tap a button to open the timeline in a modal view so they can edit it.

Timeline List Edit Mode

The issue was that when I closed the modal view with a button, sometimes the EditButton on the list view would stop working. Looking at the view hierarchy in debug mode I could see that a containing object for the button was way out of position, and thus the button would stop working. I could sometimes get it snap back into position by swiping up and down on the list, but that is not really a viable solution. I needed a way to solve this.

This is a video I send to Apple as part of the feedback report (FB7265174). You can see the out-of-place container object.

I was once again banging my head on this issue today, trying to see if I could come up with a solution. While working through some options, I noticed that this only happed to SwiftUI Buttons, not to other types of objects in the navigation bar. I made a simple Text view and gave it an onTagGuesture modifier with a call to toggle the edit mode.

Text(self.listEditMode.isEditing ? "Done" : "Edit")
    .onTapGesture {
        self.listEditMode.isEditing ? .inactive : .active
    }

This worked, but I lost the nice animations that SwiftUI was taking care of for me. I came across this post on StackOverflow and modified my view to use this bool version of Edit Mode instead of the the default one that SwiftUI provides. Here is a rough approximation

struct ContentView: View {

    @State var isEditing = false

    var names = ["Item 1", "Item 2", "Item 3"]

    var body: some View {
        NavigationView {
            VStack {
                List(names, id: \.self) { name in
                    Text(name)
                }
                .environment(\.editMode, .constant(self.isEditing ? EditMode.active : EditMode.inactive)).animation(Animation.spring())
                
            }
            .navigationBarItems(trailing: trailingButton)
        }
    }
    
    private var trailingButton: some View {
        
        Text(self.isEditing == true ? "Done" : "Edit")
            .contentShape(Rectangle())
            .frame(height: 44, alignment: .trailing)
            .onTapGesture {
                self.$isEditing.wrappedValue.toggle()
        }

    }
}

This sample code strips away all of the additional logic from my Timeline List view, but it shows how to implement the alternative version of the edit button as a Text view. Finally I have a reliable way to toggle edit mode in SwiftUI.