SwiftUI异步函数中Alert未触发问题求助
问题描述
我开发的应用里有个认证表单,未登录时自动显示,登录后隐藏,也能手动调出切换服务器。

我在网络控制器类NetBrain里定义了@Published属性showAlert,绑定到表单的Alert控件,用来展示连接失败的提示。但现在遇到问题:用户点击Connect按钮后,showAlert被设为true,Alert却不会立即弹出,得再次点击Connect才会显示。网络连接是异步操作,我猜是延迟导致的,但我原以为绑定@Published属性的Alert会在布尔值变化时自动更新。
我的理解是不是错了?有没有更好的实现方式,能在网络错误确认后及时给用户显示提示?
主网络控制器类代码
@MainActor class NetBrain: ObservableObject { @Published var showAlert = false @Published var alertTitle = "" @Published var alertMessage = "" @Published var tokenStatusCode = 0 @Published var authToken = "" @Published var password = "" func encodeBase64(username: String, password: String) -> String? { let credential = "\(username):\(password)" return credential.data(using: .utf8)?.base64EncodedString() } func getToken(server: String, username: String, password: String) async throws -> JamfAuthToken { print("Getting token - Netbrain") guard let base64 = encodeBase64(username: username, password: password) else { print("Error encoding username/password") throw NetError.couldntEncodeNamePass } guard var components = URLComponents(string: server) else { throw JamfAPIError.badURL } components.path="/api/v1/auth/token" guard let url = components.url else { throw JamfAPIError.badURL } var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("Basic \(base64)", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Accept") // send request and get data guard let (data, response) = try? await URLSession.shared.data(for: request) else { throw JamfAPIError.requestFailed } // check the response code self.tokenStatusCode = (response as? HTTPURLResponse)?.statusCode ?? 0 if self.tokenStatusCode != 200 { print("Code not 200") self.alertMessage = "Could not authenticate. Please check the url and authentication details" self.alertTitle = "Authentication Error" self.showAlert = true print("getToken showAlert is set as:\(showAlert)") throw JamfAPIError.http(self.tokenStatusCode) } // MARK: Parse JSON returned let decoder = JSONDecoder() guard let auth = try? decoder.decode(JamfAuthToken.self, from: data) else { throw JamfAPIError.decode } print("We have a token") self.authToken = auth.token return auth } func atSeparationLine() { print("----------------------------------------") } } enum NetError: Error { case couldntEncodeNamePass } enum JamfAPIError: Error { case badURL case requestFailed case http(Int) case decode } struct JamfAuthToken: Codable { let token: String }
连接表单代码
// ConnectSheet.swift // JamfListApp // // Created by Amos Deane on 15/04/2024. // import Foundation import SwiftUI struct ConnectSheet: View { @Binding var show: Bool @EnvironmentObject var networkController: NetBrain @State var saveInKeychain: Bool = false //Alert @State private var showAlert = false @State private var alertMessage = "" @State private var alertTitle = "" @State private var showActivity = false @AppStorage("server") var server = "" @AppStorage("username") var username = "" var body: some View { VStack { VStack { Form { HStack { Label("", systemImage: "globe") TextField("Server", text: $server) .disableAutocorrection(true) #if os(iOS) .autocapitalization(.none) .textInputAutocapitalization(.never) #endif } .frame(width: 400) HStack { Label("", systemImage: "person") TextField("Username", text: $username) .disableAutocorrection(true) #if os(iOS) .autocapitalization(.none) .textInputAutocapitalization(.never) #endif } HStack { Label("", systemImage: "ellipsis.rectangle") SecureField("Password", text: $networkController.password) .disableAutocorrection(true) #if os(iOS) .autocapitalization(.none) .textInputAutocapitalization(.never) #endif } Toggle("Save in Keychain", isOn: $saveInKeychain) } }.padding() #if os(macOS) HStack { Spacer() Button("Cancel") { show = false } .keyboardShortcut(.escape) HStack(spacing:30) { Button("Connect") { Task { await connect() } print("Pressing button") print("networkController.showAlert is set as:\(networkController.showAlert)") print("showAlert is set as:\(showAlert)") } .alert(isPresented: $networkController.showAlert, content: { showCustomAlert(alertTitle: networkController.alertTitle, alertMessage: networkController.alertMessage ) }) } }.padding() #else HStack { Button("Cancel") { show = false } .buttonStyle(.borderedProminent) .tint(.blue) } #endif } .alert(isPresented: $showAlert) { showCustomAlert(alertTitle: alertTitle, alertMessage: alertMessage) } } func connect() async { #if os(macOS) if saveInKeychain { try? Keychain.save(password: networkController.password, service: server, account: username) } #else if saveInKeychain { try? networkController.updateKC(networkController.password, account: server, service: username) } #endif await handleConnect(server: server, username: username, password: networkController.password) } func showCustomAlert(alertTitle: String, alertMessage: String ) -> Alert { print("Running showCustomAlert ") return Alert( title: Text(alertTitle), message: Text(alertMessage), dismissButton: .default(Text("OK")) ) } func handleConnect(server: String, username: String, password: String ) async { print("Running: handleConnect") if password.isEmpty { print("Try to get password from keychain") #if os(macOS) guard let pwFromKeychain = try? Keychain.getPassword(service: server, account: username) else { print("pwFromKeychain failed") self.alertTitle = "Error" self.alertMessage = "Failed to retrieve password from keychain" self.showAlert = true return } networkController.password = pwFromKeychain print("pwFromKeychain succeeded") #else guard let pwFromKeychain = try? networkController.getPassword(account: username, service: server) else { print("pwFromKeychain failed") self.alertTitle = "Error" self.alertMessage = "Failed to retrieve password from keychain" self.showAlert = true return } networkController.password = pwFromKeychain print("pwFromKeychain succeeded") #endif } networkController.atSeparationLine() print("Handling connection - initial connection to Jamf") do { try await networkController.getToken(server: server, username: username, password: password) if networkController.authToken != "" { print("Token status is:\(networkController.tokenStatusCode)") print("Token is:\(networkController.authToken)") // 认证成功后隐藏表单 show = false } } catch { print("Error is:\(error)") // 捕获到错误时,复用ViewModel的Alert信息 self.alertTitle = networkController.alertTitle self.alertMessage = networkController.alertMessage self.showAlert = true } } } // 模拟Keychain工具类 enum Keychain { static func save(password: String, service: String, account: String) throws { // 实际实现 } static func getPassword(service: String, account: String) throws -> String { // 实际实现 return "" } }
问题分析与解决方法
核心问题原因
- Sheet提前隐藏导致Alert失效:原代码中
connect()方法先执行show = false隐藏了表单Sheet,而Alert绑定在Sheet内部的Button上,Sheet被销毁后,对应的Alert视图结构也不存在,自然无法弹出。 - Alert状态管理混乱:同时维护了ViewModel的
showAlert和View自身的showAlert两个状态,错误发生时两者的触发逻辑没有对齐,导致Alert无法正确响应。
优化实现方案
方案1:调整Sheet隐藏时机,保留Sheet展示Alert
把show = false移到认证成功的分支中,失败时保留Sheet并弹出Alert:
- 修改
connect()方法,移除提前隐藏Sheet的代码 - 在
handleConnect的成功分支添加隐藏Sheet的逻辑 - 在View中添加自身Alert的绑定,确保错误时能触发弹窗
方案2:全局Alert管理(推荐)
如果希望Sheet隐藏后仍能弹出Alert,可以将Alert绑定到App主视图,用ViewModel统一管理弹窗状态:
- 在
NetBrain中定义Alert配置结构体,替代单独的showAlert、alertTitle、alertMessage:
@MainActor class NetBrain: ObservableObject { @Published var alertConfig: AlertConfig? struct AlertConfig: Identifiable { let id = UUID() let title: String let message: String } // 触发认证错误弹窗 func showAuthError() { self.alertConfig = AlertConfig( title: "Authentication Error", message: "Could not authenticate. Please check the url and authentication details" ) } }
- 在
getToken的错误分支调用showAuthError()替代原有状态设置 - 在App主视图中绑定这个Alert:
@main struct JamfListApp: App { @StateObject private var networkController = NetBrain() var body: some Scene { WindowGroup { MainView() .environmentObject(networkController) .alert(item: $networkController.alertConfig) { config in Alert( title: Text(config.title), message: Text(config.message), dismissButton: .default(Text("OK")) ) } } } }
这种方式不受Sheet生命周期影响,只要ViewModel触发Alert配置更新,主视图就会弹出弹窗,逻辑更清晰。
额外建议
- 移除冗余的Alert状态变量,统一用ViewModel管理弹窗信息
- 添加加载状态(比如
ProgressView),在认证过程中显示,提升用户体验
内容的提问来源于stack exchange,提问作者boristheslug
相关产品推荐
相关产品推荐

