Swift/Combine: Setting up Services

In this article I’ll present a solution what we worked out to start required services that depend on each other. While this is not very spectactular and solutions are available, we focused on setting this up using the new Combine features, especially Futures and Promises.
The example I will use consists of a CloudService, checking if iCloud is available, and a DatabaseService, that sets up a CoreData storage depending on the iCloud availability.
Be warned: The story will take some of your time to read and understand. It reflects my current understanding, so it is possible that there are things that a more experienced programmer might optimize. However, I believe that it may help you to enter this kind of reactive solutions with the relatively new Combine framework.
What’s the basic idea behind the approach?
- The
AppDelegate
shall be kept as clean as possible. - There shall be a method how the services can be accessed from anywhere. This allows, for instance, to have the service check the availability of iCloud in case of changes, and to have some consumer react on that change.
So, the first idea was to set up a ServiceRegister
where the required services can be registered at. To allow easy access lateron, there shall be an enum
that provides the keys for a Dictionary
that stores the services.
So simple the idea, so non-working is it. The problem is that the protocol has associated types, so the compiler complains:
We can solve this problem by wrapping the protocol into a struct:
To have this generic struct as value for the dictionary, we have to change its value type:
The registration function changes as well:
Calling the registration now looks like this:
So, now we can register our services conforming to the Service
protocol, wrapped by the AnyService
structure. And we can start the services by just calling startServices()
of the ServiceRegister
.
But: How shall that startServices()
shall work? We want to achieve two goals:
- Have the service work in the background, as it may take some time for them to process, and they shall not block the main task.
- Have a handling for dependencies between the services; in our case: have the database service wait for the cloud service deliver the cloud status.
To achieve this, we can use Combine’s Future
. The CloudService
‘s start
function is then defined like this:
The start()
function returns a Future
with a promise. In the ServiceRegister
‘s startServices()
function we can take this promise and react on its result, when they are available:
First, we get the cloud service from the registry. With the .start()
operator we trigger its execution.
The .sink
lets us react on the result and has two parts:
- The
receiveCompletion
delivers the overall status, especially errors, where we can react on by checking.failure
. - The
receiveValue
delivers the value in case of successful execution. In our case, we want to set a property with the iCloud’s availability status.
This makes the code pretty clean, but when you’re used to classic programming paradigms, like me, this is kind of unusual thinking. The major advantage I see is that we’re getting rid of closures, and we can let errors “bubble up” until we really want to handle them.
But we’re not yet finished. We still have to start the database service, depending on the iCloud status.
What we do is set up the DatabaseService
in a similar way (I will not show that here, as it’s pretty similar to the code above).
Given that, we can enhance the startServices()
function:
If we have received a value from the cloudService
, we’re taking the databaseService
and perform the same operations as before on it. The result here is that we provide a databaseStorageType
to a property of the ServiceRegister
, so that we can access that from somewhere else.
Actually, we’re done.
Just one more thing: You might have noticed the .store(in: &self.cancellables)
statements. Why do we need those?
Well, I stumbled about the fact that the services started, but they never delivered a result. After some googling, I found out that the promise was not kept alive long enough.
You can enforce that by adding a property to your class using promises:
private var cancellables = Set<AnyCancellable>()
You then just have to add the above .store
operation at the very end of the chain, and you’re done.
Quite some stuff to read and understand — I hope you enjoyed it and you can take some advantage of that. If you have any comments or ideas, please let me know. And perhaps you may want to clap — I’d really appreciate that!