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
|
2 Answers
Reset to default 02 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
main network controller class
, how you declare it and how you pass it to theconnect sheet
. The bits of code you show is not enough. – workingdog support Ukraine Commented Mar 10 at 11:57.task(id: connect)
and don't even need a class. – malhal Commented Mar 10 at 17:33ConnectSheet
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
inNetBrain
. Try using just the one in yourNetBrain
and remove the others. – workingdog support Ukraine Commented Mar 10 at 23:55