Small Talk
Small Talk

Reputation: 829

Row not deleted until Edit Mode button pressed in SwiftUI Grouped Table

I implemented a Grouped table in SwiftUI using an ObservableObject as the data source. A nested ForEach is used to generate each section. An EditMode() button toggles that Environment property. In Edit mode, when delete action is completed, the deleted row (unexpectedly)remains on screen. (Even though the object has been removed from the data source array.) When the user returns to normal viewing mode the object is belatedly removed from the table.

enter image description here

In order to try to track down bug:

So the simple question is what did I do wrong that would cause SwiftUI not to immediately reflect delete action in EditMode on a very simple (I think) grouped (by Section) List?

struct ContentView: View {

    @EnvironmentObject var vm: AppData

    var body: some View {

        NavigationView {

            List {
                ForEach(vm.folderSource) { (folder: Folder)   in
                    return Section(header: Text(folder.title)) {
                        //this is where problem originates. When I drop in a new full-fledged View struct, UI updates stop working properly when .onDelete is called from this nested View
                        FolderView(folder: folder)
                    }
                }
            }.listStyle(GroupedListStyle())
                .navigationBarItems(trailing: EditButton())
        }
    }
}

struct FolderView: View {

    var folder: Folder

    @EnvironmentObject var vm: AppData


    var body: some View {
        //I'm using a dedicated View inside an outer ForEach loop to be able to access a data-source for each dynamic view.

        let associatedProjects = vm.projects.filter{$0.folder == folder}

        return ForEach(associatedProjects) { (project: Project) in
            Text(project.title.uppercased())
            // dumbed-down delete, to eliminate other possible issues preventing accurate Dynamic View updates
        }.onDelete{index in self.vm.delete()}
    }
}


//view model
class AppData: ObservableObject {



    let folderSource: [Folder]
    @Published var projects: [Project]

    func delete() {
        //dumbed-down static delete call to try to find ui bug
        self.projects.remove(at: 0)
        //
    }


    init() {
        let folders = [Folder(title: "folder1", displayOrder: 0), Folder(title: "folder2", displayOrder: 1), Folder(title: "folder3", displayOrder: 2)  ]

        self.folderSource = folders


        self.projects = {

            var tempArray = [Project]()
            tempArray.append(Project(title: "project 0", displayOrder: 0, folder: folders[0]  ))
            tempArray.append(Project(title: "project 1", displayOrder: 1, folder: folders[0]  ))
            tempArray.append(Project(title: "project 2", displayOrder: 2, folder: folders[0]  ))


            tempArray.append(Project(title: "project 3", displayOrder: 0, folder: folders[1]  ))
            tempArray.append(Project(title: "project 4", displayOrder: 1, folder: folders[1]  ))
            tempArray.append(Project(title: "project 5", displayOrder: 2, folder: folders[1]  ))


            tempArray.append(Project(title: "project 6", displayOrder: 0, folder: folders[2]  ))
            tempArray.append(Project(title: "project 7", displayOrder: 1, folder: folders[2]  ))
            tempArray.append(Project(title: "project 8", displayOrder: 2, folder: folders[2]  ))

            return tempArray
        }()

    }

}


//child entity many-to-one (Folder)
class Project: Hashable, Equatable, Identifiable {

    let id = UUID()
    let title: String
    let displayOrder: Int
    let folder: Folder

    init(title: String, displayOrder: Int, folder: Folder) {
        self.title = title
        self.displayOrder = displayOrder
        self.folder = folder
    }

    static func == (lhs: Project, rhs: Project) -> Bool {
        lhs.id == rhs.id

    }

    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
}

//parent entity: Many Projects have one Folder
class Folder: Hashable, Equatable, Identifiable {

    let id = UUID()
    let title: String
    let displayOrder: Int


    init(title: String, displayOrder: Int) {
        self.title = title
        self.displayOrder = displayOrder
    }

    //make Equatable
    static func == (lhs: Folder, rhs: Folder) -> Bool {
        lhs.id == rhs.id
    }

    //make Hashable
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
}

And in SceneDelegate.swift

 // Create the SwiftUI view that provides the window contents.
        let contentView = ContentView().environmentObject(AppData())

Upvotes: 1

Views: 1275

Answers (2)

Small Talk
Small Talk

Reputation: 829

So in a strange twist, @kontiki's (helpful) solution worked by pure coincidence. It turns out that simply adding an (unused) function-type variable to FolderView as View property parameter and using that function parameter to set a State/Environment-type wrapped variable in init method solves the issue. Which is inexplicable.

WORKS (add function parameter that sets wrapped state property ['vm' is the variable name for the AppData view model, which conforms to ObservableObject]. See above.)

FolderView(folder: folder, onDelete: {self.vm.hello = "ui update bug goes away, even though this function not called"}) //function sets EnvironmentObject-type property

DOESN'T WORK (add function parameter that does NOT set wrapped state property

FolderView(folder: folder, onDelete: {print("ui update bug still here")})

DOESN'T WORK (add non-function parameter)

FolderView(folder: folder, unusedString: "ui update bug still here") 

I filed a bug report, since (to my mind) this is unexpected behavior.

Upvotes: 0

kontiki
kontiki

Reputation: 40579

I deleted my previous answer, since as you noted, although it worked it was just pure coincidence.

Here you have another work around. It basically works by not encapsulating the second ForEach. So far I found that encapsulating is a good tool for evading certain bugs. In this case it is the opposite!

struct ContentView: View {

    @EnvironmentObject var vm: AppData

    var body: some View {

        NavigationView {

            List {
                ForEach(vm.folderSource) { (folder: Folder)   in
                    Section(header: Text(folder.title)) {
//                        FolderView(folder: folder)
                        ForEach(self.vm.projects.filter{$0.folder == folder}) { (project: Project) in
                            Text(project.title.uppercased())
                        }.onDelete{index in
                            self.vm.delete()
                        }
                    }
                }
            }
            .listStyle(GroupedListStyle())
            .navigationBarItems(trailing: EditButton())
        }
    }
}

Upvotes: 3

Related Questions