用于解码数据的 SwiftUI Combine Publisher targetstruct
SwiftUI Combine Publisher targetstruct for decoding data
我在 SwiftUI 中使用 Combine 发出 API 请求然后解码数据并返回它时遇到了一些困难。当调用 API 服务时,它在 'AnyPublisher<UserLoginResponse, APIError>' 中声明结果将是这种类型。但是,我想重用 API 服务并将响应解码为不同的模型结构。如何在定义必须将返回数据解码到的数据结构时调用 API 服务?例如,在另一个 ViewModel 中,我想将 API 数据解码为 'NewsUpdatesResponse' 而不是 'UserLoginResponse'。我现在的代码如下:
大部分代码来自:tundsdev
API 服务
struct APIService {
func request(from endpoint: APIRequest, body: String) -> AnyPublisher<UserLoginResponse, APIError> {
var request = endpoint.urlRequest
request.httpMethod = endpoint.method
if endpoint.authenticated == true {
request.setValue("testToken", forHTTPHeaderField: "token")
}
if body != "" {
let finalBody = body.data(using: .utf8)
request.httpBody = finalBody
}
return URLSession
.shared
.dataTaskPublisher(for: request)
.receive(on: DispatchQueue.main)
.mapError { _ in APIError.unknown}
.flatMap { data, response -> AnyPublisher<UserLoginResponse, APIError> in
guard let response = response as? HTTPURLResponse else {
return Fail(error: APIError.unknown).eraseToAnyPublisher()
}
print(response.statusCode)
if response.statusCode == 200 {
let jsonDecoder = JSONDecoder()
return Just(data)
.decode(type: UserLoginResponse.self, decoder: jsonDecoder)
.mapError { _ in APIError.decodingError }
.eraseToAnyPublisher()
}
else {
return Fail(error: APIError.errorCode(response.statusCode)).eraseToAnyPublisher()
}
}
.eraseToAnyPublisher()
}
}
登录 ViewModel
class LoginViewModel: ObservableObject {
@Published var loginState: ResultState = .loading
private var cancellables = Set<AnyCancellable>()
private let service: APIService
init(service: APIService) {
self.service = service
}
func login(username: String, password: String) {
self.loginState = .loading
let cancellable = service
.request(from: .login, body: "username=admin&password=admin")
.sink { res in
print(res)
switch res {
case .finished:
self.loginState = .success
case .failure(let error):
self.loginState = .failed(error: error)
}
} receiveValue: { response in
print(response)
}
self.cancellables.insert(cancellable)
}
}
以下内容未经测试,但您可以使用通用 Decodable 沿着这条线尝试一些东西:
struct APIService {
func request<T: Decodable>(from endpoint: APIRequest, body: String) -> AnyPublisher<T, APIError> {
var request = endpoint.urlRequest
request.httpMethod = endpoint.method
if endpoint.authenticated == true {
request.setValue("testToken", forHTTPHeaderField: "token")
}
if body != "" {
let finalBody = body.data(using: .utf8)
request.httpBody = finalBody
}
return URLSession
.shared
.dataTaskPublisher(for: request)
.receive(on: DispatchQueue.main)
.mapError { _ in APIError.unknown}
.flatMap { data, response -> AnyPublisher<T, APIError> in // <-- here
guard let response = response as? HTTPURLResponse else {
return Fail(error: APIError.unknown).eraseToAnyPublisher()
}
print(response.statusCode)
if response.statusCode == 200 {
let jsonDecoder = JSONDecoder()
return Just(data)
.decode(type: T.self, decoder: jsonDecoder) // <-- here
.mapError { _ in APIError.decodingError }
.eraseToAnyPublisher()
}
else {
return Fail(error: APIError.errorCode(response.statusCode)).eraseToAnyPublisher()
}
}
.eraseToAnyPublisher()
}
}
您可能还想 return 一个这样的 Decodable 数组:
func requestThem<T: Decodable>(from endpoint: APIRequest, body: String) -> AnyPublisher<[T], APIError> {
....
.flatMap { data, response -> AnyPublisher<[T], APIError> in
...
.decode(type: [T].self, decoder: jsonDecoder)
...
在 workingdog 的帮助下,对我有用的最终解决方案如下。
API 服务
struct APIService {
func request<T: Decodable>(ofType type: T.Type, from endpoint: APIRequest, body: String) -> AnyPublisher<T, Error> {
var request = endpoint.urlRequest
request.httpMethod = endpoint.method
if endpoint.authenticated == true {
request.setValue("testToken", forHTTPHeaderField: "token")
}
if body != "" {
let finalBody = body.data(using: .utf8)
request.httpBody = finalBody
}
return URLSession
.shared
.dataTaskPublisher(for: request)
.receive(on: DispatchQueue.main)
.mapError { _ in Error.unknown}
.flatMap { data, response -> AnyPublisher<T, Error> in
guard let response = response as? HTTPURLResponse else {
return Fail(error: Error.unknown).eraseToAnyPublisher()
}
print(response.statusCode)
let jsonDecoder = JSONDecoder()
if response.statusCode == 200 {
return Just(data)
.decode(type: T.self, decoder: jsonDecoder)
.mapError { _ in Error.decodingError }
.eraseToAnyPublisher()
}
else {
do {
let errorMessage = try jsonDecoder.decode(APIErrorMessage.self, from: data)
return Fail(error: Error.errorCode(statusCode: response.statusCode, errorMessage: errorMessage.error ?? "Er is iets foutgegaan")).eraseToAnyPublisher()
}
catch {
return Fail(error: Error.decodingError).eraseToAnyPublisher()
}
}
}
.eraseToAnyPublisher()
}
}
登录 ViewModel
class LoginViewModel: ObservableObject {
@Published var loginState: ResultState = .loading
private var cancellables = Set<AnyCancellable>()
private let service: APIService
init(service: APIService) {
self.service = service
}
func login(username: String, password: String) {
self.loginState = .loading
let preparedBody = APIPrepper.prepBody(parametersDict: ["username": username, "password": password])
let cancellable = service.request(ofType: UserLoginResponse.self, from: .login, body: preparedBody).sink { res in
switch res {
case .finished:
self.loginState = .success
print(self.loginState)
case .failure(let error):
self.loginState = .failed(stateIdentifier: error.statusCode, errorMessage: error.errorMessage)
print(self.loginState)
}
} receiveValue: { response in
print(response)
}
self.cancellables.insert(cancellable)
}
}
请注意,我同时对用户名和密码参数的传递做了一些小改动。
我在 SwiftUI 中使用 Combine 发出 API 请求然后解码数据并返回它时遇到了一些困难。当调用 API 服务时,它在 'AnyPublisher<UserLoginResponse, APIError>' 中声明结果将是这种类型。但是,我想重用 API 服务并将响应解码为不同的模型结构。如何在定义必须将返回数据解码到的数据结构时调用 API 服务?例如,在另一个 ViewModel 中,我想将 API 数据解码为 'NewsUpdatesResponse' 而不是 'UserLoginResponse'。我现在的代码如下:
大部分代码来自:tundsdev
API 服务
struct APIService {
func request(from endpoint: APIRequest, body: String) -> AnyPublisher<UserLoginResponse, APIError> {
var request = endpoint.urlRequest
request.httpMethod = endpoint.method
if endpoint.authenticated == true {
request.setValue("testToken", forHTTPHeaderField: "token")
}
if body != "" {
let finalBody = body.data(using: .utf8)
request.httpBody = finalBody
}
return URLSession
.shared
.dataTaskPublisher(for: request)
.receive(on: DispatchQueue.main)
.mapError { _ in APIError.unknown}
.flatMap { data, response -> AnyPublisher<UserLoginResponse, APIError> in
guard let response = response as? HTTPURLResponse else {
return Fail(error: APIError.unknown).eraseToAnyPublisher()
}
print(response.statusCode)
if response.statusCode == 200 {
let jsonDecoder = JSONDecoder()
return Just(data)
.decode(type: UserLoginResponse.self, decoder: jsonDecoder)
.mapError { _ in APIError.decodingError }
.eraseToAnyPublisher()
}
else {
return Fail(error: APIError.errorCode(response.statusCode)).eraseToAnyPublisher()
}
}
.eraseToAnyPublisher()
}
}
登录 ViewModel
class LoginViewModel: ObservableObject {
@Published var loginState: ResultState = .loading
private var cancellables = Set<AnyCancellable>()
private let service: APIService
init(service: APIService) {
self.service = service
}
func login(username: String, password: String) {
self.loginState = .loading
let cancellable = service
.request(from: .login, body: "username=admin&password=admin")
.sink { res in
print(res)
switch res {
case .finished:
self.loginState = .success
case .failure(let error):
self.loginState = .failed(error: error)
}
} receiveValue: { response in
print(response)
}
self.cancellables.insert(cancellable)
}
}
以下内容未经测试,但您可以使用通用 Decodable 沿着这条线尝试一些东西:
struct APIService {
func request<T: Decodable>(from endpoint: APIRequest, body: String) -> AnyPublisher<T, APIError> {
var request = endpoint.urlRequest
request.httpMethod = endpoint.method
if endpoint.authenticated == true {
request.setValue("testToken", forHTTPHeaderField: "token")
}
if body != "" {
let finalBody = body.data(using: .utf8)
request.httpBody = finalBody
}
return URLSession
.shared
.dataTaskPublisher(for: request)
.receive(on: DispatchQueue.main)
.mapError { _ in APIError.unknown}
.flatMap { data, response -> AnyPublisher<T, APIError> in // <-- here
guard let response = response as? HTTPURLResponse else {
return Fail(error: APIError.unknown).eraseToAnyPublisher()
}
print(response.statusCode)
if response.statusCode == 200 {
let jsonDecoder = JSONDecoder()
return Just(data)
.decode(type: T.self, decoder: jsonDecoder) // <-- here
.mapError { _ in APIError.decodingError }
.eraseToAnyPublisher()
}
else {
return Fail(error: APIError.errorCode(response.statusCode)).eraseToAnyPublisher()
}
}
.eraseToAnyPublisher()
}
}
您可能还想 return 一个这样的 Decodable 数组:
func requestThem<T: Decodable>(from endpoint: APIRequest, body: String) -> AnyPublisher<[T], APIError> {
....
.flatMap { data, response -> AnyPublisher<[T], APIError> in
...
.decode(type: [T].self, decoder: jsonDecoder)
...
在 workingdog 的帮助下,对我有用的最终解决方案如下。
API 服务
struct APIService {
func request<T: Decodable>(ofType type: T.Type, from endpoint: APIRequest, body: String) -> AnyPublisher<T, Error> {
var request = endpoint.urlRequest
request.httpMethod = endpoint.method
if endpoint.authenticated == true {
request.setValue("testToken", forHTTPHeaderField: "token")
}
if body != "" {
let finalBody = body.data(using: .utf8)
request.httpBody = finalBody
}
return URLSession
.shared
.dataTaskPublisher(for: request)
.receive(on: DispatchQueue.main)
.mapError { _ in Error.unknown}
.flatMap { data, response -> AnyPublisher<T, Error> in
guard let response = response as? HTTPURLResponse else {
return Fail(error: Error.unknown).eraseToAnyPublisher()
}
print(response.statusCode)
let jsonDecoder = JSONDecoder()
if response.statusCode == 200 {
return Just(data)
.decode(type: T.self, decoder: jsonDecoder)
.mapError { _ in Error.decodingError }
.eraseToAnyPublisher()
}
else {
do {
let errorMessage = try jsonDecoder.decode(APIErrorMessage.self, from: data)
return Fail(error: Error.errorCode(statusCode: response.statusCode, errorMessage: errorMessage.error ?? "Er is iets foutgegaan")).eraseToAnyPublisher()
}
catch {
return Fail(error: Error.decodingError).eraseToAnyPublisher()
}
}
}
.eraseToAnyPublisher()
}
}
登录 ViewModel
class LoginViewModel: ObservableObject {
@Published var loginState: ResultState = .loading
private var cancellables = Set<AnyCancellable>()
private let service: APIService
init(service: APIService) {
self.service = service
}
func login(username: String, password: String) {
self.loginState = .loading
let preparedBody = APIPrepper.prepBody(parametersDict: ["username": username, "password": password])
let cancellable = service.request(ofType: UserLoginResponse.self, from: .login, body: preparedBody).sink { res in
switch res {
case .finished:
self.loginState = .success
print(self.loginState)
case .failure(let error):
self.loginState = .failed(stateIdentifier: error.statusCode, errorMessage: error.errorMessage)
print(self.loginState)
}
} receiveValue: { response in
print(response)
}
self.cancellables.insert(cancellable)
}
}
请注意,我同时对用户名和密码参数的传递做了一些小改动。