如何在 SwiftUI URLSession 中显示来自服务器的错误消息

Posted

技术标签:

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

我正在构建一个与 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) 但我还没有将它们设置到视图中。 【参考方案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 对这种失礼感到抱歉。我想为可能再次遇到这种情况的人提供确切的代码,尽管它是您的变体。我已将您的标记为已接受。

以上是关于如何在 SwiftUI URLSession 中显示来自服务器的错误消息的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Swift 中使用 URLSession 缓存图像

如何在 Swift 3 中使用带有代理的 URLSession

如何模拟 URLSession.DataTaskPublisher

Swift:如何在异步 urlsession 函数中返回一个值?

Swift:如何在异步 urlsession 函数中返回一个值?

如何使用 URLSession 解析并将它们导入 Swift 的数组中?