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.
}
- 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.
- Because Tweet is a Codable struct we want to convert it to JSON data.
- If any error is encountered during encoding, return it to our publisher.
- 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.
- 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.
- Our client then performs the request, and tries to return the value from the emitted APIResponse instance (we discussed it in the previous post).
- We the call
.eraseToAnyPublisher()
to hide the publisher type to the caller and expose it as anAnyPublisher
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.
