(This is the Combine version of this article. There is also an RxSwift version.)
Very occasionally you come across a different approach to your work that really shifts the way you think about things – for the better. To me Siesta, a Swift framework for iOS and macOS apps that talk to REST APIs, is that rare thing. In the words of its documentation, Siesta "untangles stateful messes and drastically simplifies app code".
You know what else untangles stateful messes? Reactive libraries like Combine and RxSwift. Both Combine and Siesta take you away from the procedural – do this, then that – to the declarative – here are my inputs, here's how to transform them into effects in the app. The two technologies fit together really well; Siesta's user guide even says "It would be the most natural thing in the world to wire a Siesta resource up to a reactive library".
I've been using Siesta in my projects for years, and it's been a year or so since I started using it with reactive programming, during which time the combination has really proven itself. Time to give something back.
So here we are: I've added Combine support to Siesta. It's currently a pull request against Siesta's codebase, waiting for Paul, Siesta's very busy owner, to have time to review it. But it's usable now – just point your dependency manager at github.com/luxmentis/siesta:reactive.
A preliminary note: Give it time. Siesta is a mental shift away from the usual request-based approach to REST APIs, and reactive programming is a mental shift from procedural programming, so there's definitely some time required to absorb the combination of the two. But it's worth it – the result of this apparent complexity is simplicity, and that's got to be a good thing.
A good place to start is Siesta's example app. It now contains Combine variants of the main view controllers. If you're already a Siesta user, you can get an idea of what you might gain by adopting Combine by comparing the two versions of the controllers.
The example app accepts a Github username and displays a list of that user's repositories. Let's have a look at the controller responsible for displaying the repo list.
The controller's input is a stream of Siesta Resource
s; the resource represents the retrieval of the list of a user's repositories from the Github API (or a list of recently active repos if the username is empty). We get a new Resource
whenever the username is altered. This is the main part of the code:
func configure(repositories: AnyPublisher<Resource? /* [Repository] */, Never>) {
repositories
// In this project we have an extension to tell Siesta's status overlay to be
// interested in the latest Resource output by a Resource publisher. The following
// line gives us a progress spinner, error display and retry functionality.
.watchedBy(statusOverlay: statusOverlay)
// Transform the sequence of Resources into a sequence of their content: [Repository].
.flatMapLatest { resource -> AnyPublisher<[Repository], Never> in
resource?.contentPublisher() ?? Just([]).eraseToAnyPublisher()
}
// This is everything we need to populate the table with the list of repos,
// courtesy of CombineDataSources.
.bind(subscriber: tableView.rowsSubscriber(cellIdentifier: "repo", cellType: RepositoryTableViewCell.self, cellConfig: { cell, indexPath, repo in
cell.repository = repo
}))
.store(in: &subs)
}
Here's what just happened. In a small handful of lines, we:
- displayed a loading spinner while fetching
- populated the table with the results
- displayed a suitable error message on failure, provided a retry button and handled retries
- handled data changes due to the combination of username query changes, memory cache hits and network fetch results
The publishers
There are a couple of publishers available. In this case we used Resource.contentPublisher()
. This just outputs the resource's data when it arrives, because that's all we needed in this case – loading and error handling are done for us by Siesta's ResourceStatusOverlay
.
If we were interested in a more detailed state of the resource – say because we weren't using ResourceStatusOverlay
– we'd use Resource.statePublisher()
. This gives us the changing state of the resource, including loading and error data. It's the Combine equivalent of adding a resource observer in plain Siesta.
extension Resource {
public func contentPublisher<T>() -> AnyPublisher<T,Never>
public func statePublisher<T>() -> AnyPublisher<ResourceState<T>,Never>
}
public struct ResourceState<T> {
/// Resource.latestData?.typedContent(). If the resource produces content of a different type you get an error.
public let content: T?
public let latestError: RequestError?
public let isLoading: Bool
public let isRequesting: Bool
public let latestEvent: ResourceEvent
}
Explicit content types!
Looking at the code above you'll see that, unlike Resource
, the publishers require you to imply the type of content returned by the resource. They handle conversion to that type for you, and you'll get an error (in latestError
) if the resource produces a different type.
If Siesta has a weakness, it's that Resource
doesn't have an associated content type (this was because of language constraints). This means, for example, that your API methods can't specify what type of content they return:
class _GitHubAPI {
func user(_ username: String) -> Resource
}
You now have an alternative – you can pass around publishers instead:
class _GitHubAPI {
func user(_ username: String) -> AnyPublisher<ResourceState<User>, Never>
}
Note there's a cost to this: you're stepping out of Siesta-world, and your consumers don't have the option of doing anything Resource
-related. What you decide here will come down to the needs of the moment and personal preference.
Next steps – solving complexity
Next, have a look at UserViewControllerCombine
. It performs the user lookup that is a precursor to our repo list display above, but it's a little more complicated than that:
The Github API is rate limited, so you often hit errors when playing with the app. The solution is to log in so you get a higher limit. After login, UserViewControllerCombine
reruns the current query to resolve any rate limit errors. So, authentication state as well as username feed into the user query – potential spaghetti ahead!
But have a look at the code and you'll see this all layed out really nicely because of the combination of Combine and Siesta. Potential tangles like this are an area where reactive programming really shines.
Hopefully this is enough to get you exploring the example code and seeing the potential of using Combine along with Siesta. There's more to discover, including Combine support for Request
s.