Sending data in Swift using Combine

Posting data using Combine in Swift

Posted on by Ciprian Redinciuc Ciprian Redinciuc · 3 min read

Why

I got to work more with Combine and realized there weren't many blog posts on how to send POST requests using Combine, so I decided to write a short blog post to help fellows that might be looking for help.

Most of the blogposts I saw out there were revolving around getting a simple request and that is a good start but we will definitely need to send some data to a server as well so I decided to write a blog post on how to do just that.

As I previously mentioned in this blog post, Combine is a framework that helps us write functional reactive code. We don't want to end up in a situation where half of our networking calls are written using Combine and the other half aren't.

In the following paragraphs, I will present a straightforward approach on how to send data to your server by using Combine and Codable structs.

Start with a publisher

To be able to trigger an action, we will need a publisher to emit the element we want to send to the server and we want to do it in a functional manner.

Let's build on the previous example:

/// Enum of API Errors
enum APIError: Error {
    /// Encoding issue when trying to send data.
    case encodingError(String?)
    /// No data recieved from the server.
    case noData
    /// The server response was invalid (unexpected format).
    case invalidResponse
    /// The request was rejected: 400-499
    case badRequest(String?)
    /// Encountered a server error.
    case serverError(String?)
    /// There was an error parsing the data.
    case parseError(String?)
    /// Unknown error.
    case unknown
}

func post(tweet: Tweet) -> AnyPublisher<Tweet, Error> {
    return Just(tweet) // 1.
    .encode(encoder: JSONEncoder()) // 2.
    .mapError { error -> APIError in
        // 3.
        return APIError.encodingError(error.localizedDescription)
    }
    .tryMap { jsonData -> [String: Any] in
        // 4.
        do {
            let json = try JSONSerialization.jsonObject(with: jsonData, options: [])
            guard let jsonDict =  json as? [String: Any] else {
                throw APIError.encodingError("Invalid object")
            }
            return jsonDict
        } catch {
            throw APIError.encodingError(error.localizedDescription)
        }
    }
    .map { jsonDict -> URLRequest in
        // 5.
        let request = TimelineEndpoint.create(parameters: jsonDict).urlRequest
        return request
    }
    .flatMap { request in
        // 6.
        return client.perform(request)
            .map(\.value)
    }
    .eraseToAnyPublisher() // 7.
}
  1. First of, we create a Publisher with our tweet parameter. The Just operator creates a publisher that emits just that single value we pass it as an argument.

  2. Because Tweet is a Codable struct we want to convert it to JSON data.

  3. If any error is encountered during encoding, return it to our publisher.

  4. If encoding succeeds, we want to convert our data to a dictionary as our TimelineEndpoint requires. If we cannot do that, we throw an error to our publisher.

  5. Our TimelineEndpoint struct is responsible to create our NSURLRequest and does things such as setting the HTTP headers, parameter conversion, determines the HTTP request type, in this case POST.

  6. Our client then performs the request, and tries to return the value from the emitted APIResponse instance (we discussed it in the previous post).

  7. We the call .eraseToAnyPublisher() to hide the publisher type to the caller and expose it as an AnyPublisher type.

And that's it! Now we have a bridge method between our imperative code and our functional one.


Networking with functional code

Now, we can add this to our previously created TwitterAPI:

struct TwitterAPI {
  let client: HTTPClient
  
  func getTweetsFor(screenName: String) -> AnyPublisher<Tweet, Error> {
    let request = TimelineEndpoint.getFor(screenName: screenName).urlRequest
    return client.perform(request)
                  .map(\.value)
                  .eraseToAnyPublisher()
  }

  func post(tweet: Tweet) -> AnyPublisher<Tweet, Error> {
    return Just(tweet)
        .encode(encoder: JSONEncoder())
        .mapError { error -> APIError in
            return APIError.encodingError(error.localizedDescription)
        }
        .tryMap { jsonData -> [String: Any] in
            do {
                let json = try JSONSerialization.jsonObject(with: jsonData, options: [])
                guard let jsonDict =  json as? [String: Any] else {
                    throw APIError.encodingError("Invalid object")
                }
                return jsonDict
            } catch {
                throw APIError.encodingError(error.localizedDescription)
            }
        }
        .map { jsonDict -> URLRequest in
            let request = TimelineEndpoint.create(parameters: jsonDict).urlRequest
            return request
        }
        .flatMap { request in
            return client.perform(request)
                .map(\.value)
        }
        .eraseToAnyPublisher()
    }
}

Hope you find this useful.

Want products news and updates?

Sign up for our newsletter to stay up to date.

We care about the protection of your data. Read our Privacy Policy.

© 2020 Applicodo SRL. All rights reserved.