Search code examples
swifthttp-postcombine

HTTP POST request using Swift Combine


I'm fairly new to Combine declarative API. I'm trying to implement a generic network layer for a SwiftUI application. For all requests that receive data I understand how to structure the data flow.

My problem is that I have some HTTP POST requests that returns no data. Only a HTTP 200 on success. I can't figure out how to create a publisher that will handle a decoding that can fail since there could be not data in the body of the response. Here's what I tried:

func postResource<Resource: Codable>(_ resource: Resource, to endpoint: Endpoint) -> AnyPublisher<Resource?, NetworkError> {
        return Just(resource)
            .subscribe(on: queue)
            .encode(encoder: JSONEncoder())
            .mapError { error -> NetworkError in
                return NetworkError.encoding(error)
            }
            .map { data -> URLRequest in
                return endpoint.makeRequest(with: data)
            }
            .tryMap { request -> Resource? in
                self.session.dataTaskPublisher(for: request)
                    .tryMap { data, response -> Data in
                        guard let httpUrlResponse = response as? HTTPURLResponse else { throw NetworkError.unknown }
                        guard (200 ... 299).contains(httpUrlResponse.statusCode) else { throw NetworkError.error(for: httpUrlResponse.statusCode) }
                        return data
                    }
                    .tryMap { data -> Resource? in
                        return try? JSONDecoder().decode(Resource.self, from: data)
                    }
            }
            .mapError({ error -> NetworkError in
                switch error {
                case is Swift.DecodingError:
                    return NetworkError.decoding(error)
                case let urlError as URLError:
                    return .urlError(urlError)
                case let error as NetworkError:
                    return error
                default:
                    return .unknown
                }
            })
            .eraseToAnyPublisher()
    }

The compiler is complaining with the following error on tryMap row: Declared closure result 'Publishers.TryMap<URLSession.DataTaskPublisher, Resource?>' is incompatible with contextual type 'Resource?'

Anyone has an idea? Thanks!


Solution

  • enum NetworkError: Error {
        case encoding(Error)
        case error(for: Int)
        case decoding(Error)
        case urlError(URLError)
        case unknown
    }
    
    func postResource<Resource: Codable>(_ resource: Resource, to endpoint: Endpoint) -> AnyPublisher<Resource?, NetworkError> {
        Just(resource)
            .subscribe(on: queue)
                .encode(encoder: JSONEncoder())
                .mapError { error -> NetworkError in
                   NetworkError.encoding(error)
                }
                .map { data -> URLRequest in
                   endpoint.makeRequest(with: data)
                }
                .flatMap { request in // the key thing is here you should you use flatMap instead of map
                    URLSession.shared.dataTaskPublisher(for: request)
                        .tryMap { data, response -> Data in
                            guard let httpUrlResponse = response as? HTTPURLResponse else { throw NetworkError.unknown }
                            guard 200 ... 299 ~= httpUrlResponse.statusCode else { throw NetworkError.error(for: httpUrlResponse.statusCode) }
                            return data
                        }
                        .tryMap { data -> Resource? in
                            try? JSONDecoder().decode(Resource.self, from: data)
                        }
                }
                .mapError({ error -> NetworkError in
                    switch error {
                    case is Swift.DecodingError:
                        return NetworkError.decoding(error)
                    case let urlError as URLError:
                        return .urlError(urlError)
                    case let error as NetworkError:
                        return error
                    default:
                        return .unknown
                    }
                })
                .receive(on: DispatchQueue.main)
                .eraseToAnyPublisher()
        }