无法使用与 SwiftUI 的组合从 URL 获得响应
Unable to get the response from URL using combine with SwiftUI
那是我的模型class
struct LoginResponse: Codable {
let main: LoginModel
}
struct LoginModel: Codable {
let success: Bool?
let token: String?
let message: String?
static var placeholder: LoginModel {
return LoginModel(success: nil, token: nil, message: nil)
}
}
这就是我的服务。我还有一个问题,我在这里使用了两个地图,但是当尝试删除 map.data 时,dataTaskPublisher 出现错误。下面提到的错误
Instance method 'decode(type:decoder:)' requires the types 'URLSession.DataTaskPublisher.Output' (aka '(data: Data, response: URLResponse)') and 'JSONDecoder.Input' (aka 'Data') be equivalent
class LoginService {
func doLoginTask(username: String, password: String) -> AnyPublisher<LoginModel, Error> {
let networkQueue = DispatchQueue(label: "Networking",
qos: .default,
attributes: .concurrent)
guard let url = URL(string: Constants.URLs.baseUrl(urlPath: Constants.URLs.loginPath)) else {
fatalError("Invalid URL")
}
print("uri", url)
let body: [String: String] = ["username": username, "password": password]
let finalBody = try! JSONSerialization.data(withJSONObject: body)
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = finalBody
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
return URLSession.shared.dataTaskPublisher(for: request)
.map(\.data)
.decode(type: LoginResponse.self, decoder: JSONDecoder())
.map { [=12=].main }
.receive(on: networkQueue)
.eraseToAnyPublisher()
}
}
那是我的 contentView
Button(action: {
self.counter += 1
print("count from action", self.counter)
func loaginTask() {
_ = loginService.doLoginTask(username: "1234567890", password: "12345")
.sink(
receiveCompletion: {
print("Received Completion: \([=13=])") },
receiveValue: { doctor in
print("hhhhh")
// print("yes ", doctor.message as Any)
}
)
}
})
这就是我的 json 回复
{
"success": true,
"token": "ed48aa9b40c2d88079e6fd140c87ac61fc9ce78a",
"expert-token": "6ec84e92ea93b793924d48aa9b40c2d88079e6fd140c87ac61fc9ce78ae4fa93",
"message": "Logged in successfully"
}
由于取消,您的发布者在调用上下文下方被销毁,因为您没有保留对订阅者的引用。
要解决此问题,您必须在某处保留对订户的引用。最合适的变体在某些成员 属性 中,但是,作为变体,它也可以是独立的(如果符合您的目标),例如
func loaginTask() {
var subscriber: AnyCancellable?
subscriber = loginService.doLoginTask(username: "1234567890", password: "12345")
.sink(
receiveCompletion: { [subscriber] result in
print("Received Completion: \(result)")
subscriber = nil // << keeps until completed
},
receiveValue: { doctor in
print("hhhhh")
// print("yes ", doctor.message as Any)
}
)
}
首先,您的错误来自于您想要 return AnyPublisher<LoginModel, Error>
但您将响应映射为 .decode(type: LoginResponse.self, decoder: JSONDecoder())
,这与您的 json 不匹配响应。
第二次,我将使用基本授权作为您的 URL 请求的主体,因为它是发送带有密码的用户凭据,必须保护密码。您可以访问服务器端吗?后端如何处理这个 post 请求?
是授权还是内容类型?我会把这两个解决方案,尝试找到在服务器端设置的那个。
您的 LoginModel 必须与您的 json 响应匹配。我注意到他们缺少 expertToken:
struct LoginModel: Codable {
let success: Bool
let token: String
let expertToken: String
let message: String
enum CodingKeys: String, CodingKey {
case success
case token
case expertToken = "expert-token"
case message
}
}
所以我会这样创建 LoginService
class:
final class LoginService {
/// The request your use when the button is pressed.
func logIn(username: String, password: String) -> AnyPublisher<LoginModel, Error> {
let url = URL(string: "http://your.api.endpoints/")!
let body = logInBody(username: username, password: password)
let urlRequest = basicAuthRequestSetup(url: url, body: body)
return URLSession.shared
.dataTaskPublisher(for: urlRequest)
.receive(on: DispatchQueue.main)
.tryMap { try self.validate([=11=].data, [=11=].response) }
.decode(
type: LoginModel.self,
decoder: JSONDecoder())
.eraseToAnyPublisher()
}
/// The body for a basic authorization with encoded credentials.
func logInBody(username: String, password: String) -> String {
let body = String(format: "%@:%@",
username,
password)
guard let bodyData = body.data(using: .utf8) else { return String() }
let encodedBody = bodyData.base64EncodedString()
return encodedBody
}
/// The authorization setup
func basicAuthRequestSetup(url: URL, body: String) -> URLRequest {
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
urlRequest.setValue("Basic \(body)",
forHTTPHeaderField: "Authorization")
return urlRequest
}
/// Validation of the Data and the response.
/// You can handle response with status code for more precision.
func validate(_ data: Data, _ response: URLResponse) throws -> Data {
guard let httpResponse = response as? HTTPURLResponse else {
throw NetworkError.unknown
}
guard (200..<300).contains(httpResponse.statusCode) else {
throw networkRequestError(from: httpResponse.statusCode)
}
return data
}
/// Handle the status code errors to populate to user.
func networkRequestError(from statusCode: Int) -> Error {
switch statusCode {
case 401:
return NetworkError.unauthorized
default:
return NetworkError.unknown
}
}
/// Define your different Error here that can come back from
/// your backend.
enum NetworkError: Error, Equatable {
case unauthorized
case unknown
}
}
因此,如果您使用简单的 Content-Type,您的正文将是下面这个。从上面的代码替换 logInBody(username:password:) -> String
和 basicAuthRequestSetup(url:body:) -> URLRequest
/// Classic body for content type.
/// Keys must match the one in your server side.
func contentTypeBody(username: String, password: String) -> [String: Any] {
[
"username": username,
"password": password
] as [String: Any]
}
/// Classic Content-Type but not secure. To avoid when having
/// passwords.
func contentTypeRequestSetup(url: URL,
body: [String: Any]) -> URLRequest {
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
urlRequest.setValue("application/json",
forHTTPHeaderField: "Content-Type")
urlRequest.httpBody = try? JSONSerialization.data(withJSONObject: body)
return urlRequest
}
然后我会创建一个 ViewModel 来处理将在您的视图中传递的逻辑。
final class OnboardingViewModel: ObservableObject {
var logInService = LoginService()
var subscriptions = Set<AnyCancellable>()
func logIn() {
logInService.logIn(username: "Shubhank", password: "1234")
.sink(receiveCompletion: { completion in
print(completion) },
receiveValue: { data in
print(data.expertToken) }) // This is your response
.store(in: &subscriptions)
}
}
现在,在您的 ContentView 中,您可以在按钮内传递视图模型登录操作:
struct ContentView: View {
@ObservedObject var viewModel = OnboardingViewModel()
var body: some View {
Button(action: { viewModel.logIn() }) {
Text("Log In")
}
}
}
那是我的模型class
struct LoginResponse: Codable {
let main: LoginModel
}
struct LoginModel: Codable {
let success: Bool?
let token: String?
let message: String?
static var placeholder: LoginModel {
return LoginModel(success: nil, token: nil, message: nil)
}
}
这就是我的服务。我还有一个问题,我在这里使用了两个地图,但是当尝试删除 map.data 时,dataTaskPublisher 出现错误。下面提到的错误
Instance method 'decode(type:decoder:)' requires the types 'URLSession.DataTaskPublisher.Output' (aka '(data: Data, response: URLResponse)') and 'JSONDecoder.Input' (aka 'Data') be equivalent
class LoginService {
func doLoginTask(username: String, password: String) -> AnyPublisher<LoginModel, Error> {
let networkQueue = DispatchQueue(label: "Networking",
qos: .default,
attributes: .concurrent)
guard let url = URL(string: Constants.URLs.baseUrl(urlPath: Constants.URLs.loginPath)) else {
fatalError("Invalid URL")
}
print("uri", url)
let body: [String: String] = ["username": username, "password": password]
let finalBody = try! JSONSerialization.data(withJSONObject: body)
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = finalBody
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
return URLSession.shared.dataTaskPublisher(for: request)
.map(\.data)
.decode(type: LoginResponse.self, decoder: JSONDecoder())
.map { [=12=].main }
.receive(on: networkQueue)
.eraseToAnyPublisher()
}
}
那是我的 contentView
Button(action: {
self.counter += 1
print("count from action", self.counter)
func loaginTask() {
_ = loginService.doLoginTask(username: "1234567890", password: "12345")
.sink(
receiveCompletion: {
print("Received Completion: \([=13=])") },
receiveValue: { doctor in
print("hhhhh")
// print("yes ", doctor.message as Any)
}
)
}
})
这就是我的 json 回复
{
"success": true,
"token": "ed48aa9b40c2d88079e6fd140c87ac61fc9ce78a",
"expert-token": "6ec84e92ea93b793924d48aa9b40c2d88079e6fd140c87ac61fc9ce78ae4fa93",
"message": "Logged in successfully"
}
由于取消,您的发布者在调用上下文下方被销毁,因为您没有保留对订阅者的引用。
要解决此问题,您必须在某处保留对订户的引用。最合适的变体在某些成员 属性 中,但是,作为变体,它也可以是独立的(如果符合您的目标),例如
func loaginTask() {
var subscriber: AnyCancellable?
subscriber = loginService.doLoginTask(username: "1234567890", password: "12345")
.sink(
receiveCompletion: { [subscriber] result in
print("Received Completion: \(result)")
subscriber = nil // << keeps until completed
},
receiveValue: { doctor in
print("hhhhh")
// print("yes ", doctor.message as Any)
}
)
}
首先,您的错误来自于您想要 return AnyPublisher<LoginModel, Error>
但您将响应映射为 .decode(type: LoginResponse.self, decoder: JSONDecoder())
,这与您的 json 不匹配响应。
第二次,我将使用基本授权作为您的 URL 请求的主体,因为它是发送带有密码的用户凭据,必须保护密码。您可以访问服务器端吗?后端如何处理这个 post 请求? 是授权还是内容类型?我会把这两个解决方案,尝试找到在服务器端设置的那个。
您的 LoginModel 必须与您的 json 响应匹配。我注意到他们缺少 expertToken:
struct LoginModel: Codable {
let success: Bool
let token: String
let expertToken: String
let message: String
enum CodingKeys: String, CodingKey {
case success
case token
case expertToken = "expert-token"
case message
}
}
所以我会这样创建 LoginService
class:
final class LoginService {
/// The request your use when the button is pressed.
func logIn(username: String, password: String) -> AnyPublisher<LoginModel, Error> {
let url = URL(string: "http://your.api.endpoints/")!
let body = logInBody(username: username, password: password)
let urlRequest = basicAuthRequestSetup(url: url, body: body)
return URLSession.shared
.dataTaskPublisher(for: urlRequest)
.receive(on: DispatchQueue.main)
.tryMap { try self.validate([=11=].data, [=11=].response) }
.decode(
type: LoginModel.self,
decoder: JSONDecoder())
.eraseToAnyPublisher()
}
/// The body for a basic authorization with encoded credentials.
func logInBody(username: String, password: String) -> String {
let body = String(format: "%@:%@",
username,
password)
guard let bodyData = body.data(using: .utf8) else { return String() }
let encodedBody = bodyData.base64EncodedString()
return encodedBody
}
/// The authorization setup
func basicAuthRequestSetup(url: URL, body: String) -> URLRequest {
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
urlRequest.setValue("Basic \(body)",
forHTTPHeaderField: "Authorization")
return urlRequest
}
/// Validation of the Data and the response.
/// You can handle response with status code for more precision.
func validate(_ data: Data, _ response: URLResponse) throws -> Data {
guard let httpResponse = response as? HTTPURLResponse else {
throw NetworkError.unknown
}
guard (200..<300).contains(httpResponse.statusCode) else {
throw networkRequestError(from: httpResponse.statusCode)
}
return data
}
/// Handle the status code errors to populate to user.
func networkRequestError(from statusCode: Int) -> Error {
switch statusCode {
case 401:
return NetworkError.unauthorized
default:
return NetworkError.unknown
}
}
/// Define your different Error here that can come back from
/// your backend.
enum NetworkError: Error, Equatable {
case unauthorized
case unknown
}
}
因此,如果您使用简单的 Content-Type,您的正文将是下面这个。从上面的代码替换 logInBody(username:password:) -> String
和 basicAuthRequestSetup(url:body:) -> URLRequest
/// Classic body for content type.
/// Keys must match the one in your server side.
func contentTypeBody(username: String, password: String) -> [String: Any] {
[
"username": username,
"password": password
] as [String: Any]
}
/// Classic Content-Type but not secure. To avoid when having
/// passwords.
func contentTypeRequestSetup(url: URL,
body: [String: Any]) -> URLRequest {
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
urlRequest.setValue("application/json",
forHTTPHeaderField: "Content-Type")
urlRequest.httpBody = try? JSONSerialization.data(withJSONObject: body)
return urlRequest
}
然后我会创建一个 ViewModel 来处理将在您的视图中传递的逻辑。
final class OnboardingViewModel: ObservableObject {
var logInService = LoginService()
var subscriptions = Set<AnyCancellable>()
func logIn() {
logInService.logIn(username: "Shubhank", password: "1234")
.sink(receiveCompletion: { completion in
print(completion) },
receiveValue: { data in
print(data.expertToken) }) // This is your response
.store(in: &subscriptions)
}
}
现在,在您的 ContentView 中,您可以在按钮内传递视图模型登录操作:
struct ContentView: View {
@ObservedObject var viewModel = OnboardingViewModel()
var body: some View {
Button(action: { viewModel.logIn() }) {
Text("Log In")
}
}
}