Networking and API Structure using Combine
Nowadays, even simple apps rely on Internet connectivity and no matter how hard networking theory may be, doing connectivity has always been simplified via URLSession.
As the JSON format increase in popularity, Apple has started to develop a way to combine Swift code with JSON data, giving the birth to Codable, making decoding and encoding really easy.
URLSession is the preferred way to perform network data transfer tasks. It offers a modern asynchronous API with powerful configuration options and fully transparent backgrounding support. Some of the operations supported are:
- Data transfer tasks to retrieve the content of a URL
- Download tasks to fetch data from a URL and save it to a file
- Upload tasks to upload data to a URL
- Stream task to stream data between two endpoints
At the moment only the first one, the data transfer task exposes a Combine publisher, making integration with the reactive code pretty straightforward.
Here’s an example:
URLSession provides the dataTaskPublisher(for:) which returns a publisher that wraps a URL session data task for a given URL.
When the task completes, it publishes either:
- A tuple that contains the fetched data and a URLResponse, if the task succeeds
- An error, if the task fails
Please notice that, unlike the non-combine dataTask method dataTask(with:completionHandler:), the types received by your code aren’t optionals, since the publisher has already unwrapped the data or error.
Using the Combine publisher is very useful because instead of making all the work inside the completionHandler, you can split up the work and analyse data and response in a separate method the the completion, leaving all the work to the framework. Once completed the data task successfully, a block of raw Data is delivered to the receiveValue closure. Combine provides an easy way to convert the useless Data block in a collection of useful objects via Codable.
The tuple received by the receiveValue is (Data, URLResponse). Multiple ways are allowed to convert these types to others needed by you. For example, you may use:
- map(_:), an operator that can be applied on a publisher , which converts the content of this tuple to another type.
- tryMap(_:), another operator, which allows to inspect the response before doing any work.
- In case you need to decode raw data, the decode(type:decoder:) operator is your choice. It works easily by specifying the Type of the object (note that it must adopt the Decodable protocol) and the designed JSONDecoder.
This example uses both the tryMap in order to check if there are any error (in case it does, a URLError is thrown), and the decode operator that decodes JSON data into Article an object. What happens when you need to add multiple subscribers to a given publisher? Using this approach is not really performance friendly. In fact, for every subscriber the network operations are remade.
As developers, we want that the download operation is performed only once and distributed to all subscribers without the need to fetch the same data multiple times. To solve this problem, two solutions can be applied:
- Use the share() operator, but it’s not optimal because you need to subscribe all your subscribers before the result comes back
- Use the multicast() operator, which retrieves a ConnectablePublisher that publishes value through a Subject. Using this operator, you can subscribe all the operators that you need, and when you’re done, call the connect() method.
The ConnectablePublisher is defined by Apple as “a publisher that provides an explicit means of connecting and canceling publication”. With “explicit means” it considers that a custom subject can be used in order to publish values. The publisher doesn’t produce any element until you call its connect() method. This call is performed after you’ve created all the subscriptions that you need to a given publisher.
This approach allows the publisher to produce its value only once and share its data to all the subscribers.
API Structure Example
Now that we’ve seen how URLSession can be used in combination with publishers, we’re ready to perform some real work. We will use a funny API called ChuckNorris.io in order to perform the requests.
We’ll work with a Playground file in order to make it easier to implement and test.
Structuring an API is not straightforward sometimes, but lets use a simple structure that adapts to pretty much every situation. First of all, an Enum to specify the possible errors that may arise during the networking processing.
We make it adopt LocalizedError and specify the errorDescription for every case in order to provide a meaningful description of the problem verified.
APIs provides more endpoints to deal with, sometimes with parameters, sometimes with a fixed structure.
Having another enum just for Endpoints may be the right solution.
Let’s go through the methods called and see what they do:
- dataTaskPublisher — returns a publisher for a given URL as mentioned before in the article
- receive(on:) — allows to switch to the background queue, instead of performing blocking operations on the main one.
- map() — map the current output to only the data from the resulting tuple
- decode() — easy way to decode your Data in a ChuckQuote
- catch() — for this example, we convert any error in an Empty publisher, which means no error will be published
- eraseToAnyPublisher() — exposes an instance of AnyPublisher to the downstream subscriber, in order to preserve abstraction across API boundaries.
Using catch allows to:
- Emit the value and complete if you get back a ChuckQuote
- Return, in this case, an Empty publisher which completes successfully without emitting any values, in case of a failure
Once we’ve defined the basic structure for the API, it’s time to test it and see how it behaves in case of success and failure.
Using the sink() function on the AnyPublisher returned by the API, we’re able to perform operations with both the completion received and the values. The assign() method could have been also used, but it’s not the right fit in our case.
Remember to store the subscriber into an array of AnyCancellable objects. Using this approach, it allows the subscription to not be cancelled when the execution leaves the scope of the function in which the sink() function is called.
It’s time for you! Feel free to edit methods, implement the other two endpoints and test other operators!
To see all the code used in this article, visit the following page:
If you want to explore other Combine’s topics, visit our repository on GitHub: