I have a SwiftUI app that utilizes swipe to delete on a list of users. My problem is that when I do this, a random element of the array gets selected and deleted.
My first thought was that the index was getting assigned erroneously, but stepping through the code I think the index function is working properly. It seems to be something in the selection of the row.
Any thoughts?
Here's my code:
import SwiftUI
struct AdminUserTableView: View {
@State var userViewModel = UserViewModel.shared
@State var deleteAdminUserMessage = ""
@State var deleteAdminAlert = false
@State var deleteConfirm = false
@State var getUsersMessage = ""
@State var getUsersAlert = false
var body: some View {
Text("")
.navigationTitle("Admin Users")
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
NavigationLink
{
AddUserView()
} label: {
Text("+")
.font(.largeTitle)
}
}
}
List {
ForEach(userViewModel.adminUsers, id: \.self) { user in
AdminUserCellView(user: user)
.swipeActions {
Button("Delete") {
deleteConfirm = true
}
.tint(.red)
}
.confirmationDialog("Are you sure you want to delete \(user.email)?", isPresented: $deleteConfirm, titleVisibility: .visible) {
Button("Delete", role: .destructive) {
Task {
do {
//Delete user from server
var httpStatus = try await NetworkController.shared.deleteAdminUser(user: user)
let index = UserViewModel.shared.adminUsers.firstIndex(where: { $0.id == user.id })!
//Delete user from array.
userViewModel.removeUser(at: index)
} catch {
deleteAdminUserMessage = "Unable to delete user. Please try again."
deleteAdminAlert = true
}
}
}
Button("Cancel", role: .cancel) {}
}
}
}
.alert(deleteAdminUserMessage, isPresented: $deleteAdminAlert) {
Button("OK", role: .cancel) { }
}
.onAppear {
Task {
do {
UserViewModel.shared.adminUsers = try await NetworkController.shared.getAdminUsers()
} catch {
getUsersMessage = "Failed to get admin users from server. Go back, then try again."
getUsersAlert = true
}
}
}
}
}
Here's UserViewModel:
import Foundation
@Observable class UserViewModel {
static var shared = UserViewModel()
var adminUsers: [User] = []
var residentUsers: [ResidentUser] = []
func addAdminUser(_ user: User) {
adminUsers.append(user)
}
func removeUser(at index: Int) {
adminUsers.remove(at: index)
}
}
Edited to add: id: .self to the forEach. This did not change anything.
I found a potential partial solution, to bind the confirmation dialogue to the user. The problem remains that it's still not deleting the correct user. But it solves half the problem. Here's the code:
import SwiftUI
struct AdminUserTableView: View {
@State private var userViewModel = UserViewModel()
@State var deleteAdminUserMessage = ""
@State var deleteAdminAlert = false
// @State var deleteConfirm = false
@State var userToDelete: User? = nil
@State var getUsersMessage = ""
@State var getUsersAlert = false
var body: some View {
Text("")
.navigationTitle("Admin Users")
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
NavigationLink
{
AddUserView()
} label: {
Text("+")
.font(.largeTitle)
}
}
}
List {
ForEach(userViewModel.adminUsers, id: \.self) { user in
AdminUserCellView(user: user)
.swipeActions {
Button("Delete") {
userToDelete = user
}
.tint(.red)
}
.confirmationDialog("Are you sure you want to delete \(userToDelete?.email ?? "")?", isPresented: Binding<Bool>(
get: { userToDelete != nil },
set: { if !$0 { userToDelete = nil } }
), titleVisibility: .visible) {
Button("Delete", role: .destructive) {
if let userToDelete = userToDelete {
Task {
do {
var httpStatus = try await NetworkController.shared.deleteAdminUser(user: user)
if httpStatus.statusCode == 200 {
let index = userViewModel.adminUsers.firstIndex(where: { $0.id == user.id })!
userViewModel.removeUser(at: index)
}
} catch {
deleteAdminUserMessage = "Unable to delete user. Please try again."
deleteAdminAlert = true
}
}
}
}
Button("Cancel", role: .cancel) {}
}
}
}
.alert(deleteAdminUserMessage, isPresented: $deleteAdminAlert) {
Button("OK", role: .cancel) { }
}
.onAppear {
Task {
do {
userViewModel.adminUsers = try await NetworkController.shared.getAdminUsers()
} catch {
getUsersMessage = "Failed to get admin users from server. Go back, then try again."
getUsersAlert = true
}
}
}
}
}
Try this approach using one source of truth data model, @State private var userViewModel = UserViewModel()
,
and a dedicated @State private var selected: User?
to
ensure the correct deletion of the desired user.
Example code:
struct ContentView: View {
var body: some View {
NavigationStack {
AdminUserTableView()
}
}
}
struct AdminUserTableView: View {
@State private var userViewModel = UserViewModel() // <--- here
@State private var deleteAdminUserMessage = ""
@State private var deleteAdminAlert = false
@State private var deleteConfirm = false
@State private var getUsersMessage = ""
@State private var getUsersAlert = false
@State private var selected: User? // <--- here
var body: some View {
Text("xxxxx") // for my testing
.navigationTitle("Admin Users")
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
NavigationLink
{
// AddUserView()
Text("AddUserView")
} label: {
Text("+")
.font(.largeTitle)
}
}
}
List {
ForEach(userViewModel.adminUsers) { user in
// AdminUserCellView(user: user)
Text(user.name)
.swipeActions {
Button("Delete") {
selected = user // <--- here
deleteConfirm = true
}
.tint(.red)
}
.confirmationDialog("Are you sure you want to delete \(selected?.name ?? "not valid")?", isPresented: $deleteConfirm, titleVisibility: .visible) {
Button("Delete", role: .destructive) {
Task {
do {
if let selected { // <--- here
//Delete user from server
//var httpStatus = try await NetworkController.shared.deleteAdminUser(user: selected)
userViewModel.deleteAdminUser(selected)
}
} catch {
deleteAdminUserMessage = "Unable to delete user. Please try again."
deleteAdminAlert = true
}
}
}
Button("Cancel", role: .cancel) {}
}
}
}
.alert(deleteAdminUserMessage, isPresented: $deleteAdminAlert) {
Button("OK", role: .cancel) { }
}
.task { // <--- here
do {
// userViewModel.adminUsers = try await NetworkController.shared.getAdminUsers()
// for my testing
userViewModel.adminUsers = [User(name: "user-0"),User(name: "user-1"),User(name: "user-2"),User(name: "user-3"),User(name: "user-4"),User(name: "user-5")]
} catch {
getUsersMessage = "Failed to get admin users from server. Go back, then try again."
getUsersAlert = true
}
}
}
}
@Observable class UserViewModel {
var adminUsers: [User] = []
var residentUsers: [ResidentUser] = []
func deleteAdminUser(_ user: User) {
if let index = adminUsers.firstIndex(where: { $0.id == user.id }) {
adminUsers.remove(at: index)
}
}
}
struct User: Identifiable {
let id = UUID()
var name: String
init(name: String) {
self.name = name
}
}
struct ResidentUser: Identifiable {
let id = UUID()
var name: String
init(name: String) {
self.name = name
}
}