technicalMay 24, 2021

Sending data in Swift using Combine

Article presentation

Why

Today we are going to show you how to send data in Swift using Combine. But first what is Combine? 

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. 

Since 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 if you might need it. 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.

Continue reading if you what to find a straightforward approach on how to send data to your server by using Combine and Codable structs.

Start with a publisher

In order 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 the following example:

 1 /// Enum of API Errors
 2 enum APIError: Error {
 3     /// Encoding issue when trying to send data.
 4     case encodingError(String?)
 5     /// No data recieved from the server.
 6     case noData
 7     /// The server response was invalid (unexpected format).
 8     case invalidResponse
 9     /// The request was rejected: 400-499
10     case badRequest(String?)
11     /// Encountered a server error.
12     case serverError(String?)
13     /// There was an error parsing the data.
14     case parseError(String?)
15     /// Unknown error.
16     case unknown
17 }
18 
19 func post(tweet: Tweet) -> AnyPublisher {
20     return Just(tweet) // 1.
21     .encode(encoder: JSONEncoder()) // 2.
22     .mapError { error -> APIError in
23         // 3.
24         return APIError.encodingError(error.localizedDescription)
25     }
26     .tryMap { jsonData -> [String: Any] in
27         // 4.
28         do {
29             let json = try JSONSerialization.jsonObject(with: jsonData, options: [])
30             guard let jsonDict =  json as? [String: Any] else {
31                 throw APIError.encodingError("Invalid object")
32             }
33             return jsonDict
34         } catch {
35             throw APIError.encodingError(error.localizedDescription)
36         }
37     }
38     .map { jsonDict -> URLRequest in
39         // 5.
40         let request = TimelineEndpoint.create(parameters: jsonDict).urlRequest
41         return request
42     }
43     .flatMap { request in
44         // 6.
45         return client.perform(request)
46             .map(\.value)
47     }
48     .eraseToAnyPublisher() // 7.
49 }
  1. First of all, 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.

This is it! We now have a bridge method between our imperative code and our functional one.

Networking with functional code

It's time to add this to our previously created TwitterAPI:

 1 struct TwitterAPI {
 2   let client: HTTPClient
 3   
 4   func getTweetsFor(screenName: String) -> AnyPublisher {
 5     let request = TimelineEndpoint.getFor(screenName: screenName).urlRequest
 6     return client.perform(request)
 7                   .map(\.value)
 8                   .eraseToAnyPublisher()
 9   }
10 
11   func post(tweet: Tweet) -> AnyPublisher {
12     return Just(tweet)
13         .encode(encoder: JSONEncoder())
14         .mapError { error -> APIError in
15             return APIError.encodingError(error.localizedDescription)
16         }
17         .tryMap { jsonData -> [String: Any] in
18             do {
19                 let json = try JSONSerialization.jsonObject(with: jsonData, options: [])
20                 guard let jsonDict =  json as? [String: Any] else {
21                     throw APIError.encodingError("Invalid object")
22                 }
23                 return jsonDict
24             } catch {
25                 throw APIError.encodingError(error.localizedDescription)
26             }
27         }
28         .map { jsonDict -> URLRequest in
29             let request = TimelineEndpoint.create(parameters: jsonDict).urlRequest
30             return request
31         }
32         .flatMap { request in
33             return client.perform(request)
34                 .map(\.value)
35         }
36         .eraseToAnyPublisher()
37     }
38 }

So this is how you send POST requests using Combine, hope you find it useful and instructing.

More articles from Ciprian Redinciuc can be found on Userdesk