In the previous post, we’ve implemented Middleware support to facilitate asynchronous operations and extended functionality, and today we will be following up with Error handling and improving our User Experience.
Handling errors in our Middleware Functions
Let’s say, for example, a user taps the button, and the AnimalMiddleware tries to fetch an animal from the service, but in this case, it gets an error.
When this happens, the UI will freeze, displaying the “Loading” message, and the user will be stuck waiting for a response.
To fix things, we will break them first, so let’s start by updating our AnimalService to fail randomly.
The first thing we need to do is define an enum that will hold the Error Types the service can return. Then, update our Publisher declaration to return a Failure by replacing <String, Never> with <String, AnimalServiceError>
Then, we will add some simple logic to return errors at random.
Setting up App State and Actions
Now that the service is ready, we need to set up our App State to maintain information about fetching Animals and tracking errors.
We will also take this opportunity to streamline some of the State Variables’ names and actions for clarity. This seems like changing a lot, but it’s actually just changing variable names across existing files. The compiler will guide you if you missed something.
Let’s update the AppState first:
- currentAnimal is now current
And since we’ve added new State variables, we would also need to create the corresponding Actions.
- fetchAnimal is now fetch
- setCurrentAnimal is now fetchComplete
- New action added fetchError
Then change the switch statement in the animalReducer:
And the actions we are publishing at the Middleware:
And lastly, the actions names we are dispatching from the Views.
Catching errors with Combine
When using Combine, you are probably running lots of asynchronous code, and therefore you will need ways to handle and catch errors when they appear.
Combine offers some operators to handle errors returned from upstream publishers, allowing you to recover from them. Let’s try a couple!.
This operator is the easiest of all. Every publisher that can fail (as our AnimalService Publisher) allows you to replace any error that occurs with a default value.
Let’s use replaceError, to set up a Default Action:
As the map operator fails, we will return a new Action that has a default value of “Oops,” which will cause that text to render in the Label.
Since the AnimalService can fail with different types of errors, we might want to show different information to the user. Using replaceError is not enough.
Catch allows you to inspect the received error and return a new Publisher that will be passed downstream.
Let’s update the Middleware to use .catch().
Note that we are returning a Publisher (Just), and not a plain Action, and switching the different error Types to return a slightly different one.
Configuring the Views
Now that we have proper state variables to display everything we need in the View, let’s make some changes to improve the user experience:
- After the user taps the button, we will display a ProgressView while an animal is loaded.
- If there is an error, we will present an Alert, with a custom message depending on the error that occurred.
SwiftUI on iOS 14 (beta) supports the new ProgressView. (You had to use UIKit before), so let’s add it to our AnimalView.
I have added a new ProgressView and wrapped it around a condition, that observes the State Variable fetchInprogress.
Now, the only thing we need to do is change that variable to true while fetching an animal and reset it when done. That happens in the Reducer.
We are getting there!
So far, we have decent error handling, but things can be a lot better. Let’s stop displaying errors in the Label and show an Alert with details instead.
To present an Alert in SwiftUI, you have to use some view’s State property (a Bool). When that property is true, the Alert will show, and tapping the Dismiss Button on it, will toggle the property back to false, hiding it.
In this case, we want to display an Alert based on whether the FetchError variable in our State is different than nil, so there a couple of things to consider.
- Our State variable is not a Boolean (It’s an String), so we’ll need to add some logic.
- We cannot use the fetchError variable directly because the State is read-only (remember?), and therefore the Alert dismiss action cannot change it. That means we will have to dispatch an Action to reset it via a custom Binding.
What we did:
- Created a shouldDisplayError custom binding. When read, it returns the value of the State variable, and when set, it dispatches an action to reset the existing error.
- Added an alert at the bottom, that uses the new custom binding in the isPresentedproperty to show/hide the Alert.
Note that in the custom binding set parameter, we ignore whatever value comes from the Alert and are just dispatching an action.
We are getting closer now. Now let’s modify the Actions we are dispatching from the AnimalMiddleware when there’s an error.
I have created a new AnimalMiddlewareError type to define additional error types we may need later on and then simply modified our catch statement to return them accordingly.
And finally, let’s update the Reducer to mutate the state based on each error.
Pretty cool, huh? 😎
Conclusion and Next Steps
There is always room to continue improving error handling, such as retrying fetching the data automatically, adding localizations, and more. Taking time to correctly handle errors in your app, especially in the UI, will always be welcomed by your users.
This tutorial completes the initial “Redux-like architecture” series, but stay tuned for new stuff coming up related to better managing your SwiftUI views, and other improvements to this approach.
Posts in this series
Sources & Refs: