Search code examples
swiftswiftuialamofirereactive-programmingcombine

SwiftUI - Combine and Alamofire parse error Response


I have this general implementation for my api .post method:

func post<T: Decodable>(endpoint: Endpoint, parameters: Parameters, responseType: T.Type) -> AnyPublisher<T, Error> {
    sessionManager.request(self.baseUrl + endpoint.path, method: .post, parameters: parameters)
        .publishDecodable(type: responseType)
        .value()
        .mapError(ServiceError.init(error: ))
        .eraseToAnyPublisher()
}

It's working and I think it's how it supposed to be in SwiftUI/Combine. My problem is that I want to be able parse response when I get other status codes then 2xx. I fount this answer and this article about how to do it.

With that help I was able to change my code to this:

func post<T: Decodable>(endpoint: Endpoint, parameters: Parameters, responseType: T.Type, completionHandler: @escaping (Result<T, ErrorResponse>) -> Void) {
    sessionManager.upload(multipartFormData: multipartFormData, to: self.baseUrl + endpoint.path, method: .post, headers: headers)
        .validate(statusCode: 200..<300)
        .responseTwoDecodable(of: responseType, completionHandler: completionHandler)
}

For more info here is my TwoDecodableResponseSerializer (created with help from that two links):

final class TwoDecodableResponseSerializer<T: Decodable>: ResponseSerializer {
     
    lazy var decoder: JSONDecoder = {
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .iso8601
        return decoder
    }()
    
    private lazy var successSerializer = DecodableResponseSerializer<T>(decoder: decoder)
    private lazy var errorSerializer = DecodableResponseSerializer<ErrorResponse>(decoder: decoder)
    
    public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> Result<T, ErrorResponse> {
        
        guard let response = response else { return .failure(ErrorResponse()) }
        
        do {
            if response.statusCode < 200 || response.statusCode >= 300 {
                let result = try errorSerializer.serialize(request: request, response: response, data: data, error: nil)
                return .failure(result)
            } else {
                let result = try successSerializer.serialize(request: request, response: response, data: data, error: nil)
                return .success(result)
            }
        } catch(let err) {
            return .failure(ErrorResponse())
        }
        
    }
}

extension DataRequest {
    @discardableResult func responseTwoDecodable<T: Decodable>(queue: DispatchQueue = DispatchQueue.global(qos: .userInitiated), of t: T.Type, completionHandler: @escaping (Result<T, ErrorResponse>) -> Void) -> Self {
        return response(queue: .main, responseSerializer: TwoDecodableResponseSerializer<T>()) { response in
            switch response.result {
            case .success(let result):
                completionHandler(result)
            case .failure(let error):
                completionHandler(.failure(ErrorResponse()))
            }
        }
    }
}

and ErrorResponse:

class ErrorResponse: Error, Decodable {
    
    var error: Int = 0
    
    enum CodingKeys: String, CodingKey {
        case error = "error"
    }
    
}

and it's working. That's great but I would like to have more reactive/Combine code. Not with closure. Is it possible? How would you do this?

Thanks for any help


Solution

  • If you're using your own ResponseSerializer you can use the publishResponse(using:on:) method to use it with Combine.