如何在 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 3 中使用带有代理的 URLSession
如何模拟 URLSession.DataTaskPublisher
Swift:如何在异步 urlsession 函数中返回一个值?