【问题标题】:How to Display Error Message from Server in SwiftUI URLSession如何在 SwiftUI URLSession 中显示来自服务器的错误消息
【发布时间】:2023-04-09 20:10:02
【问题描述】:

我正在构建一个与 API 交互的 SwiftUI 应用。用户通过访问我的 Rails 服务器的注册表单创建一个帐户。可以正常工作,但我不知道如何显示来自服务器的错误消息。

例如,注册表单中的电子邮件地址和用户名需要是唯一的,并且与现有用户的不匹配。这可能是一个罕见的问题,但服务器实际上返回了一条 422 消息,表明用户创建失败,因为用户名或电子邮件或两者都已存在。

如何显示这些特定错误而不是典型的通用 URLSession 枚举错误?

另外,有没有办法在注册功能运行之前检查用户名和电子邮件地址是否已经存在,因为这是设置它的理想方式。它在网页版上就是这样工作的。

这是尝试失败后来自服务器的错误 JSON:

{
"status": "error",
"data": {
    "id": null,
    "email": "sampleuser3@example.com",
    "created_at": null,
    "updated_at": null,
    "username": "SampleUser3"

},
"errors": {
    "email": [
        "has already been taken"
    ],
    "username": [
        "has already been taken"
    ],
    "full_messages": [
        "Email has already been taken",
        "Username has already been taken"
    ]
}
}

这是我发出注册请求的注册服务:

import Foundation

struct SignUpRequestBody: Codable {
let email: String
let username: String
let firstName: String
let lastName: String
let phoneNumber: String
let password: String
let passwordConfirmation: String
let category: String
}


final class SignUpService {

static let shared = SignUpService()

func signUp(email: String, username: String, firstName: String, lastName: String, phoneNumber: String,  password: String, passwordConfirmation: String, category: String, completed: @escaping (Result<SessionToken, AuthenticationError>) -> Void) {
        
    guard let url = URL(string: "https://example.com/auth") else {
        completed(.failure(.custom(errorMessage:"URL unavailable")))
        return
    }

    let body = SignUpRequestBody(email: email.lowercased(), username: username, firstName: firstName, lastName: lastName, phoneNumber: phoneNumber, password: password, passwordConfirmation: passwordConfirmation, category: "consumer")
    
    var request = URLRequest(url: url)
            request.httpMethod = "POST"
            request.addValue("application/json", forHTTPHeaderField: "Content-Type")
            request.httpBody = try? JSONEncoder().encode(body)
    
    URLSession.shared.dataTask(with: request) { (data, response, error) in
        
        if let response = response as? HTTPURLResponse {

            guard let token = response.value(forHTTPHeaderField: "access-token") else {
                completed(.failure(.custom(errorMessage: "Missing Access Token")))
                return
            }
            guard let client = response.value(forHTTPHeaderField: "client") else {
                completed(.failure(.custom(errorMessage: "Missing Client")))
                return
            }
      
            guard let data = data, error == nil else { return }
            guard let loginResponse = try? JSONDecoder().decode(LoginResponse.self, from: data) else { return }

            guard let messageData = loginResponse.data else {return}
            guard let message = messageData.message else {return}
    
            guard let userRole = messageData.user else {return}
            guard let role = userRole.role else {return}
        
            let sessionToken = SessionToken(accessToken: token, client: client, message: message, role: role)
            
            completed(.success(sessionToken))
        }
    }.resume()
}
}

【问题讨论】:

  • 如何将此代码与 SwiftUI 集成?你如何显示Alert
  • @loremipsum 目前我还没有设置警报。我有一个带有警报的枚举: enum AuthenticationError: Error { case invalidCredentials case custom(errorMessage: String) } 但我还没有将它们设置到视图中。

标签: json swift swiftui httpresponse urlsession


【解决方案1】:

如何显示这些特定错误而不是典型的通用 URLSession 枚举错误?

没有看到LoginResponse 结构,我的猜测是:

try? JSONDecoder().decode(LoginResponse.self, from: data)

失败了。

我看到了两种处理方法:

  • 更新LoginResponse 使其也能够处理错误消息的结构(使用可选属性等)
  • 创建一个单独的 ErrorResponse 结构来处理错误消息的结构,并在解码到 LoginResponse 失败时尝试解码到该结构

我的观点是第二个更干净,如果返回错误,您会立即知道,而不是在解码后检查loginResponse 变量的结构。

ErrorResponse 结构可能类似于:

struct ErrorResponse: Codable {
  let status: String
  let data: Dictionary<String, String?>
  let errors: Dictionary<String, [String]>
}

data的解码可以修改为:

guard let loginResponse = try? JSONDecoder().decode(LoginResponse.self, from: data) else {
  guard let errorResponse = try? JSONDecoder().decode(ErrorResponse.self, from: data) else {
    completed(.failure(.custom(errorMessage: "Unable to decode as either `LoginResponse` or `ErrorResponse`")))
    return
  }

  completed(.failure(.custom(errorMessage: errorResponse.errors.description)))
  return
}

另外一个好处是您可以更改 LoginResponse 以减少可选属性,然后考虑到这种更强的类型,您可以删除函数中的大部分后续保护子句。

另外,有没有办法在注册功能运行之前检查用户名和电子邮件地址是否已经存在,因为这是设置它的理想方式。它在网页版上就是这样工作的。

如果 Rails 应用程序使用 API 端点对username 等进行预验证,那么当然,您应该能够使用相同的端点提前检查。只需在开发人员工具打开的情况下访问该网站,看看它会提出什么请求。

【讨论】:

  • 谢谢。我会尝试这个改变。
【解决方案2】:

我最终接受了上述答案并将其纳入我的服务电话中。我需要添加结构以匹配 JSON 错误代码。然后我需要将这些结构构建到打开选项时的错误响应中。

import Foundation

struct SignUpRequestBody: Codable {
let email: String
let username: String
let firstName: String
let lastName: String
let phoneNumber: String
let password: String
let passwordConfirmation: String
let category: String
}


// MARK: - BadSignUpResponse
struct BadSignUpResponse: Error, Codable {
let status: String
let data: DataClass
let errors: Errors
}

struct DataClass: Codable {
let id: Int?
let email: String?
let createdAt, updatedAt: String?
let firstName, lastName, streetAddress1, streetAddress2: String?
let city, state: String?
let country: String?
let username, companyName: String?
let zipCode, phoneNumber, stripeCustID: String?
let availableCredits: Int?
let provider, uid: String?
let allowPasswordChange, removeMyAccount: Bool?
}

// MARK: - Errors
struct Errors: Codable {
let email: [String]?
let username: [String]?
let full_messages: [String]?
}



final class SignUpService {

static let shared = SignUpService()

func signUp(email: String, username: String, firstName: String, lastName: String, phoneNumber: String,  password: String, passwordConfirmation: String, category: String, completed: @escaping (Result<SessionToken, BadSignUpResponse>) -> Void) {
        
    guard let url = URL(string: "https://example.com/api/v1/auth") else {
        completed(.failure(BadSignUpResponse.init(status: "url error",
                                                  data: DataClass.init(id: 0, email: "", createdAt: "", updatedAt: "",
                                                                       firstName: "", lastName: "", streetAddress1: "", streetAddress2: "", city: "", state: "", country: "", username: "", companyName: "", zipCode: "", phoneNumber: "", stripeCustID: "", availableCredits: 0, provider: "", uid: "", allowPasswordChange: false, removeMyAccount: false),

                                                  errors: Errors.init(email: [], username: [],
                                                                      full_messages: []))))
        return
    }

    let body = SignUpRequestBody(email: email.lowercased(), username: username, firstName: firstName, lastName: lastName, phoneNumber: phoneNumber, password: password, passwordConfirmation: passwordConfirmation, category: "consumer")
    
    var request = URLRequest(url: url)
            request.httpMethod = "POST"
            request.addValue("application/json", forHTTPHeaderField: "Content-Type")
            request.httpBody = try? JSONEncoder().encode(body)
    
    URLSession.shared.dataTask(with: request) { (data, response, error) in
        
        if let response = response as? HTTPURLResponse {
            
            guard let data = data, error == nil else { return }
            
            guard let token = response.value(forHTTPHeaderField: "access-token")  else {
                guard let errorResponse = try? JSONDecoder().decode(BadSignUpResponse.self, from: data) else {
                    completed(.failure(BadSignUpResponse.init(status: "error",
                                                              data: DataClass.init(id: 0, email: "", createdAt: "", updatedAt: "",
                                                                                   firstName: "", lastName: "", streetAddress1: "", streetAddress2: "", city: "", state: "", country: "", username: "", companyName: "", zipCode: "", phoneNumber: "", stripeCustID: "", availableCredits: 0, provider: "", uid: "", allowPasswordChange: false, removeMyAccount: false),
                                                              errors: Errors.init(email: [], username: [], full_messages: []))))
                return
            }
                completed(.failure(errorResponse))
            return
            }
        
            
            
            guard let client = response.value(forHTTPHeaderField: "client") else {
                guard let errorResponse = try? JSONDecoder().decode(BadSignUpResponse.self, from: data) else {
                    completed(.failure(BadSignUpResponse.init(status: "error",
                                                              data: DataClass.init(id: 0, email: "", createdAt: "", updatedAt: "",
                                                                                   firstName: "", lastName: "", streetAddress1: "", streetAddress2: "", city: "", state: "", country: "", username: "", companyName: "", zipCode: "", phoneNumber: "", stripeCustID: "", availableCredits: 0, provider: "", uid: "", allowPasswordChange: false, removeMyAccount: false),
                                                              errors: Errors.init(email: [], username: [], full_messages: []))))
                return
            }
                completed(.failure(errorResponse))
            return
            }
      
            guard let loginResponse = try? JSONDecoder().decode(LoginResponse.self, from: data) else {
                guard let errorResponse = try? JSONDecoder().decode(BadSignUpResponse.self, from: data) else {
                    completed(.failure(BadSignUpResponse.init(status: "login response error",
                                                              data: DataClass.init(id: 0, email: "", createdAt: "", updatedAt: "",
                                                                                   firstName: "", lastName: "", streetAddress1: "", streetAddress2: "", city: "", state: "", country: "", username: "", companyName: "", zipCode: "", phoneNumber: "", stripeCustID: "", availableCredits: 0, provider: "", uid: "", allowPasswordChange: false, removeMyAccount: false),
                                                              errors: Errors.init(email: [], username: [], full_messages: []))))
                return
            }
                completed(.failure(errorResponse))
            return
            }


            guard let messageData = loginResponse.data else {return}
            guard let message = messageData.message else {return}
    
            guard let userRole = messageData.user else {return}
            guard let role = userRole.role else {return}
        
            let sessionToken = SessionToken(accessToken: token, client: client, message: message, role: role)
            
            completed(.success(sessionToken))
        }
    }.resume()
}
}

【讨论】:

  • 总而言之,我的回答解决了您的问题,但您决定自己回答而不是接受我的回答?
  • @msbit 对这种失礼感到抱歉。我想为可能再次遇到这种情况的人提供确切的代码,尽管它是您的变体。我已将您的标记为已接受。
猜你喜欢
  • 1970-01-01
  • 2019-07-02
  • 1970-01-01
  • 2020-09-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-10-18
相关资源
最近更新 更多