Reputation: 829
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.
In order to try to track down bug:
Data source objects conform to Hashable, Identifiable, and Equatable.
A simple delete action is implemented (which is to delete the first object in the @Published property)
Data source / view model is stored in an @EnvironmentData object
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
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
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