swift - Alert not triggering on async function - Stack Overflow

I have an authentication sheet to login to my app. This appears initially if the app his not been logge

I have an authentication sheet to login to my app. This appears initially if the app his not been logged into and is then hidden, but can be called manually if you wish to change the server that you are connecting to.

I am trying to setup an alert to display for different connection failures. I am doing this by having a published property showAlert on the network controller class which is triggered by the alert on the actual sheet.

The problem I am having is that when the user clicks connect the showAlert property is set to true but the alert does not show unless the user clicks connect a second time?

The network connection is asynchronous, so I am assuming that the problem is the delay for the connection to complete.

But, my understanding of an alert was that, because it was linked to a published property it would still update when the boolean changed.

Is this wrong and/or is there an easier way to achieve this - e.g. to display the network error visually to the user when it is confirmed - even if this takes a while?

These are the relevant bits of code:

From main network controller class:


@MainActor class NetBrain: ObservableObject {

@Published var showAlert = false


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
        }
        
//        // create the request

        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
    }

From the connect sheet:


//  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)
                ////  #######################################################################
                ////  CONNECTION - button on sheet
                ////  #######################################################################
                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)")
                        alertMessage = "Could not authenticate. Please check the url and authentication details"
                        alertTitle = "Authentication Error"
                    }
                    .alert(isPresented: $networkController.showAlert,
                           content: {
                        showCustomAlert(alertTitle: networkController.alertTitle, alertMessage: networkController.alertMessage )
                    })
                }
            }.padding()
#else
            
            HStack {
                Button("Cancel") {
                    show = false
                }
                .buttonStyle(.borderedProminent)
                .tint(.blue)
            }
#endif
        }
    }

    
    func connect() async {
        // hide this sheet
        show = false
#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
                
        Task {
            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")
                return
            }
            networkController.password = pwFromKeychain
            print("pwFromKeychain succeeded")
            #else
            guard let pwFromKeychain = try? networkController.getPassword(account: username, service: server)
            else {
                print("pwFromKeychain failed")
                return
            }
            networkController.password = pwFromKeychain
            print("pwFromKeychain succeeded")
#endif
            
        }
        networkController.atSeparationLine()
        print("Handling connection - initial connection to Jamf")
        
        do {
            //  ###################################################################
            //  CONNECTION
###################################################################
            
            try await networkController.getToken(server: server, username: username, password: password)
            
            if networkController.authToken != "" {
                print("Token status is:\(networkController.tokenStatusCode)")
                print("Token is:\(networkController.authToken)")
            }
            
        } catch {
            print("Error is:\(error)")
            self.showAlert = true
        }
    }
}

I have an authentication sheet to login to my app. This appears initially if the app his not been logged into and is then hidden, but can be called manually if you wish to change the server that you are connecting to.

I am trying to setup an alert to display for different connection failures. I am doing this by having a published property showAlert on the network controller class which is triggered by the alert on the actual sheet.

The problem I am having is that when the user clicks connect the showAlert property is set to true but the alert does not show unless the user clicks connect a second time?

The network connection is asynchronous, so I am assuming that the problem is the delay for the connection to complete.

But, my understanding of an alert was that, because it was linked to a published property it would still update when the boolean changed.

Is this wrong and/or is there an easier way to achieve this - e.g. to display the network error visually to the user when it is confirmed - even if this takes a while?

These are the relevant bits of code:

From main network controller class:


@MainActor class NetBrain: ObservableObject {

@Published var showAlert = false


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
        }
        
//        // create the request

        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
    }

From the connect sheet:


//  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)
                ////  #######################################################################
                ////  CONNECTION - button on sheet
                ////  #######################################################################
                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)")
                        alertMessage = "Could not authenticate. Please check the url and authentication details"
                        alertTitle = "Authentication Error"
                    }
                    .alert(isPresented: $networkController.showAlert,
                           content: {
                        showCustomAlert(alertTitle: networkController.alertTitle, alertMessage: networkController.alertMessage )
                    })
                }
            }.padding()
#else
            
            HStack {
                Button("Cancel") {
                    show = false
                }
                .buttonStyle(.borderedProminent)
                .tint(.blue)
            }
#endif
        }
    }

    
    func connect() async {
        // hide this sheet
        show = false
#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
                
        Task {
            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")
                return
            }
            networkController.password = pwFromKeychain
            print("pwFromKeychain succeeded")
            #else
            guard let pwFromKeychain = try? networkController.getPassword(account: username, service: server)
            else {
                print("pwFromKeychain failed")
                return
            }
            networkController.password = pwFromKeychain
            print("pwFromKeychain succeeded")
#endif
            
        }
        networkController.atSeparationLine()
        print("Handling connection - initial connection to Jamf")
        
        do {
            //  ###################################################################
            //  CONNECTION
###################################################################
            
            try await networkController.getToken(server: server, username: username, password: password)
            
            if networkController.authToken != "" {
                print("Token status is:\(networkController.tokenStatusCode)")
                print("Token is:\(networkController.authToken)")
            }
            
        } catch {
            print("Error is:\(error)")
            self.showAlert = true
        }
    }
}

Share Improve this question edited Mar 10 at 18:54 boristheslug asked Mar 10 at 11:41 boristheslugboristheslug 456 bronze badges 3
  • can you show some representative code, in particular what you call the main network controller class, how you declare it and how you pass it to the connect sheet. The bits of code you show is not enough. – workingdog support Ukraine Commented Mar 10 at 11:57
  • Normally the Task would be inside of your controller class when you want it to be used from UI and non-UI code. If its only UI code then you can use .task(id: connect) and don't even need a class. – malhal Commented Mar 10 at 17:33
  • Note, your code is confusing without the relevant code. I assume that ConnectSheet is displayed in a .sheet and that you have declared @StateObject private var networkController = NetBrain() somewhere up in your view hierarchy and passed it to the .sheet specifically. What is not clear is why you have so many variables such as: show, showAlert in addition to your @Published var showAlert in NetBrain. Try using just the one in your NetBrain and remove the others. – workingdog support Ukraine Commented Mar 10 at 23:55
Add a comment  | 

2 Answers 2

Reset to default 0

2 Possible scenarios came to my mind you could check:

First, make sure you don't have multiple .alert() in the same view, it might be causing some overriding.

Second, try to set the showAlert on the main thread:

 DispatchQueue.main.async {
   self.showAlert = true
 }

I've worked out what the problem was here. I had the .alert(isPresented:) on the Connect Sheet but of course, the connect sheet was itself set to hide (via the 'show' boolean). This meant that the alert only became visible if I re-triggered the view to allow me to login again.

Before, the boolean was set correctly but there was no view to show the alert. I've fixed it by attaching the alert to a view that is permanently visible.

发布者:admin,转转请注明出处:http://www.yc00.com/questions/1744849112a4597019.html

相关推荐

  • swift - Alert not triggering on async function - Stack Overflow

    I have an authentication sheet to login to my app. This appears initially if the app his not been logge

    2天前
    40

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信