SwiftUI删除按钮报错:视图更新内禁止发布变更的原因与修复
问题描述
从数据库获取买家列表后,点击删除按钮出现两个问题:
- 弹窗显示错误的买家名称
- 控制台抛出报错:
'Publishing changes from within view updates is not allowed, this will cause undefined behavior.'
相关代码如下:
BuyersView 代码
struct BuyersView: View { @StateObject var viewModel = BuyersViewModel() @State var path = NavigationPath() var body: some View { NavigationStack(path: $path) { VStack{ CategoryView(categories: viewModel.fases, currentSelection: $viewModel.currentFase) HStack{ Spacer() NavigationLink("Add buyer", value: 0) .padding() } List(viewModel.buyers){ buyer in HStack{ NavigationLink(value: buyer) { VStack(alignment: .leading){ Text(buyer.name ?? "name") .font(.title3) Text("Fase : \(buyer.fase ?? "fase")") } } Spacer() DeleteButton(showingAlert: $viewModel.showingAlert, buyer: buyer, vm: viewModel, deleteUser: {viewModel.deleteBuyer(id: buyer.id, currentfase: viewModel.currentFase)}) } }.onChange(of: viewModel.currentFase) { _, newValue in viewModel.resetBuyers() } } .navigationDestination(for: Int.self) { _ in AddBuyerView(path: $path, fases: viewModel.fases, onBuyerAdded: { viewModel.resetBuyers()}) }.navigationDestination(for: BuyerData.self) { buyer in BuyerDetailView(buyer: buyer) } } } }
BuyersViewModel 代码
import Foundation class BuyersViewModel: ObservableObject { @Published var buyers : [BuyerData] = [] @Published var showingAlert = false @Published var currentFase : Category = .init(title: "All Buyers") let fases: [Category] = [ .init(title: "All Buyers"), .init(title: "Nieuw"), .init(title: "Email contact"), .init(title: "Gesproken"), .init(title: "Gepland"), .init(title: "Bezocht"), .init(title: "Bod"), .init(title: "Sale"), .init(title: "Zoekt niet meer") ] init() { buyers = getAllBuyers(currentfase: currentFase) } func deleteBuyer(id: Int, currentfase: Category) { let db = DBHelper() db.deleteBuyer(id: id) if currentfase.title == "All Buyers"{ buyers = db.AllBuyers() } else{ buyers = db.AllB(fase: currentfase.title) } db.close() } func getAllBuyers(currentfase: Category) -> [BuyerData]{ let db = DBHelper() var allbuyers: [BuyerData] if currentfase.title == "All Buyers"{ allbuyers = db.AllBuyers() } else{ allbuyers = db.AllB(fase: currentfase.title) } db.close() return allbuyers } func resetBuyers(){ buyers = getAllBuyers(currentfase: currentFase) } }
DeleteButton 代码
struct DeleteButton: View { @Binding var showingAlert : Bool var buyer : BuyerData? var seller: SellerData? @StateObject var vm : BuyersViewModel var deleteUser: () -> Void var messageText : String { (seller == nil ? buyer?.name : seller?.name) ?? "this user" } var titleText : String { buyer == nil ? "Seller" : "Buyer" } var body: some View { Button("delete") { vm.showingAlert = true } .buttonStyle(.borderless) .foregroundColor(.red) .alert("Deleting \(titleText)", isPresented: $showingAlert) { Button(role: .destructive) { deleteUser() vm.showingAlert = false } label: { Text("Delete") } Button(role: .cancel) { vm.showingAlert = false } label: { Text("Cancel") } } message: { Text("Are you sure you want to delete \(messageText) \n this action cannot be undone") } } }
问题原因
- 弹窗名称错误:所有
DeleteButton共用ViewModel里的同一个showingAlert变量,当多个按钮渲染完成后,点击任意按钮都会触发最后一个绑定的弹窗内容,导致显示错误的买家名称。 - 控制台报错:
deleteBuyer方法中直接修改@Published的buyers数组,而这个操作是在Alert的按钮回调中执行的——此时正处于SwiftUI的视图更新周期内,直接发布ObservableObject的变更会触发框架的限制报错。
修复方案
1. 让每个DeleteButton独立管理弹窗状态
把DeleteButton的弹窗状态从ViewModel的全局变量改为自己的@State变量,每个按钮单独控制弹窗,确保弹窗内容和当前绑定的买家匹配:
修改后的DeleteButton:
struct DeleteButton: View { var buyer : BuyerData? var seller: SellerData? var deleteUser: () -> Void // 每个按钮独立管理弹窗状态 @State private var showingAlert = false var messageText : String { (seller == nil ? buyer?.name : seller?.name) ?? "this user" } var titleText : String { buyer == nil ? "Seller" : "Buyer" } var body: some View { Button("delete") { showingAlert = true } .buttonStyle(.borderless) .foregroundColor(.red) .alert("Deleting \(titleText)", isPresented: $showingAlert) { Button(role: .destructive) { deleteUser() showingAlert = false } label: { Text("Delete") } Button(role: .cancel) { showingAlert = false } label: { Text("Cancel") } } message: { Text("Are you sure you want to delete \(messageText) \n this action cannot be undone") } } }
同时修改BuyersView中调用DeleteButton的代码,移除不必要的参数:
// 原代码 DeleteButton(showingAlert: $viewModel.showingAlert, buyer: buyer, vm: viewModel, deleteUser: {viewModel.deleteBuyer(id: buyer.id, currentfase: viewModel.currentFase)}) // 修改后 DeleteButton(buyer: buyer, deleteUser: {viewModel.deleteBuyer(id: buyer.id, currentfase: viewModel.currentFase)})
2. 异步更新ViewModel的Published变量
在deleteBuyer方法中,用DispatchQueue.main.async包装修改buyers数组的代码,确保变更操作在视图更新周期外执行,规避框架限制:
修改后的BuyersViewModel的deleteBuyer方法:
func deleteBuyer(id: Int, currentfase: Category) { let db = DBHelper() db.deleteBuyer(id: id) // 异步更新buyers数组,避免在视图更新周期内发布变更 DispatchQueue.main.async { if currentfase.title == "All Buyers"{ self.buyers = db.AllBuyers() } else{ self.buyers = db.AllB(fase: currentfase.title) } db.close() } }
最后移除ViewModel中不再使用的showingAlert变量:
class BuyersViewModel: ObservableObject { @Published var buyers : [BuyerData] = [] // 移除不再使用的变量 // @Published var showingAlert = false @Published var currentFase : Category = .init(title: "All Buyers") // ... 其他代码保持不变 }
内容的提问来源于stack exchange,提问作者Tessa Limbeek
相关产品推荐
相关产品推荐

