SwiftUI 结合 Publisher targetstruct 来解码数据
Posted
技术标签:
【中文标题】SwiftUI 结合 Publisher targetstruct 来解码数据【英文标题】:SwiftUI Combine Publisher targetstruct for decoding data 【发布时间】:2021-08-05 10:55:16 【问题描述】:我在 SwiftUI 中使用 Combine 时遇到了一些困难,它发出一个 API 请求,然后解码数据并返回它。当调用 API 服务时,它会在 'AnyPublisher
大部分代码来自: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()
登录视图模型
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)
【问题讨论】:
【参考方案1】:以下内容未经测试,但您可以尝试使用通用 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()
你可能还想返回一个这样的 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)
...
【讨论】:
感谢您的解决方案!它似乎工作,虽然我遇到了一些问题,因为我正在使用 Cancellables。对于'let cancellable:UserLoginResponse = service'这一行,我收到一个错误:'Protocol'Any'作为一种类型不能符合'Decodable'和'不能将'AnyCancellable'类型的值转换为指定类型'UserLoginResponse'。你知道如何解决这个问题吗? 尝试用.store(in: &cancellable)
替换行:self.cancellables.insert(cancellable)
我没有尝试过这个解决方案,但我自己想通了。在调用函数时缺少一个额外的参数。感谢您的帮助!【参考方案2】:
在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()
登录视图模型
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 结合 Publisher targetstruct 来解码数据的主要内容,如果未能解决你的问题,请参考以下文章
SwiftUI NSViewRepresentable 无法从@Publisher 读取数据
可以直接使用 Publisher 作为 SwiftUI 中的 @ObjectBinding 属性吗?
当 Publisher 参数没有要发出的任何新值时,为啥会调用 SwiftUI View 上的 onReceive 块?