【问题标题】:SwiftUI Combine Publisher targetstruct for decoding dataSwiftUI 结合 Publisher targetstruct 来解码数据
【发布时间】:2021-08-05 10:55:16
【问题描述】:

我在 SwiftUI 中使用 Combine 时遇到了一些困难,它发出一个 API 请求,然后解码数据并返回它。当调用 API 服务时,它会在 'AnyPublisher' 中声明结果将是这种类型。但是,我想重用 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()
    }
}

登录视图模型

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)
    }
}

【问题讨论】:

    标签: api data-structures swiftui combine publisher


    【解决方案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: &amp;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)
        }
    }
    

    请注意,与此同时,我对用户名和密码参数的传递做了一些小改动。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-07-08
      • 2020-10-01
      • 1970-01-01
      • 2015-10-06
      • 1970-01-01
      • 2020-05-27
      • 2023-01-12
      • 1970-01-01
      相关资源
      最近更新 更多