You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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 ""
    }
}

问题分析与解决方法

核心问题原因

  1. Sheet提前隐藏导致Alert失效:原代码中connect()方法先执行show = false隐藏了表单Sheet,而Alert绑定在Sheet内部的Button上,Sheet被销毁后,对应的Alert视图结构也不存在,自然无法弹出。
  2. 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统一管理弹窗状态:

  1. 在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"
        )
    }
}
  1. 在getToken的错误分支调用showAuthError()替代原有状态设置
  2. 在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 21:47:32