Enum-Driven TableView Development
In this tutorial, you will learn how to use Swift enums to handle the different states of your app to populate a table view. By Keegan Rush.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Enum-Driven TableView Development
20 mins
- Getting Started
- Different States
- Poorly Defined State
- Invalid State
- A Better Alternative
- Refactoring to a State Enum
- Refactoring the Loading State
- Refactoring the Error State
- Refactoring the Empty and Populated States
- Keeping in Sync with a Property Observer
- Adding Pagination
- How an API Supports Pagination
- Supporting Pagination in Your Table View
- Adding the New Paging State
- Setting the State to .paging
- Loading the Next Page
- Appending the Recordings
- Where to Go From Here?
Is there anything more fundamental, in iOS development, than UITableView? It’s a simple, clean control. Unfortunately, a lot of complexity lies under the hood: Your code needs to show loading indicators at the right time, handle errors, wait for service call completions and show results when they come in.
In this tutorial, you’ll learn how to use Enum-Driven TableView Development to manage this complexity.
To follow this technique, you’ll refactor an existing app called Chirper. Along the way, you’ll learn the following:
- How to use an enum to manage the state of your
ViewController
. - The importance of reflecting the state in the view for the user.
- The dangers of poorly defined state
- How to use property observers to keep your view up-to-date.
- How to work with pagination to simulate an endless list of search results.
UITableView
and Swift enums. If you need help, take a look at the iOS and Swift tutorials first.
Getting Started
The Chirper app that you’ll refactor for this tutorial presents a searchable list of bird sounds from the xeno-canto public API.
If you search for a species of bird within the app, it will present you with a list of recordings that match your search query. You can play the recordings by tapping the button in each row.
To download the starter project, use the Download Materials button at the top or bottom of this tutorial. Once you’ve downloaded this, open the starter project in Xcode.
Different States
A well-designed table view has four different states:
- Loading: The app is busy fetching new data.
- Error: A service call or another operation has failed.
- Empty: The service call has returned no data.
- Populated: The app has retrieved data to display.
The state populated is the most obvious, but the others are important as well. You should always let the user know the app state, which means showing a loading indicator during the loading state, telling the user what to do for an empty data set and showing a friendly error message when things go wrong.
To start, open MainViewController.swift to take a look at the code. The view controller does some pretty important things, based on the state of some of its properties:
- The view displays a loading indicator when
isLoading
is set totrue
. - The view tells the user that something went wrong when
error
is non-nil
. - If the
recordings
array isnil
or empty, the view displays a message prompting the user to search for something different. - If none of the previous conditions are true, the view displays the list of results.
-
tableView.tableFooterView
is set to the correct view for the current state.
There’s a lot to keep in mind while modifying the code. And, to make things worse, this pattern gets more complicated when you pile on more features through the app.
Poorly Defined State
Search through MainViewController.swift and you’ll see that the word state isn’t mentioned anywhere.
The state is there, but it’s not clearly defined. This poorly defined state makes it hard to understand what the code is doing and how it responds to the changes of its properties.
Invalid State
If isLoading
is true
, the app should show the loading state. If error
is non-nil, the app should show the error state. But what happens if both of these conditions are met? You don’t know. The app would be in an invalid state.
MainViewController
doesn’t clearly define its states, which means it may have some bugs due to invalid or indeterminate states.
A Better Alternative
MainViewController
needs a better way to manage its state. It needs a technique that is:
- Easy to understand
- Easy to maintain
- Insusceptible to bugs
In the steps that follow, you’re going to refactor MainViewController
to use an enum
to manage its state.
Refactoring to a State Enum
In MainViewController.swift, add this above the declaration of the class:
enum State {
case loading
case populated([Recording])
case empty
case error(Error)
}
This is the enum that you’ll use to clearly define the view controller’s state. Next, add a property to MainViewController
to set the state:
var state = State.loading
Build and run the app to see that it still works. You haven’t made any changes to the behavior yet so everything should be the same.
Refactoring the Loading State
The first change you’ll make is to remove the isLoading
property in favor of the state enum. In loadRecordings()
, the isLoading
property is set to true
. The tableView.tableFooterView
is set to the loading view. Remove these two lines from the beginning of loadRecordings()
:
isLoading = true
tableView.tableFooterView = loadingView
Replace it with this:
state = .loading
Then, remove self.isLoading = false
inside the fetchRecordings
completion block. loadRecordings()
should look like this:
@objc func loadRecordings() {
state = .loading
recordings = []
tableView.reloadData()
let query = searchController.searchBar.text
networkingService.fetchRecordings(matching: query, page: 1) { [weak self] response in
guard let `self` = self else {
return
}
self.searchController.searchBar.endEditing(true)
self.update(response: response)
}
}
You can now remove MainViewController’s isLoading
property. You won’t need it any more.
Build and run the app. You should have the following view:
The state
property has been set, but you’re not doing anything with it. tableView.tableFooterView
needs to reflect the current state. Create a new method in MainViewController
named setFooterView()
.
func setFooterView() {
switch state {
case .loading:
tableView.tableFooterView = loadingView
default:
break
}
}
Now, back to loadRecordings()
. After setting the state to .loading
, add the following:
setFooterView()
Build and run the app.
Now when you change the state to loading setFooterView()
is called and the progress indicator is displayed. Great job!
Refactoring the Error State
loadRecordings()
fetches recordings from the NetworkingService
. It takes the response from networkingService.fetchRecordings()
and calls update(response:)
, which updates the app’s state.
Inside update(response:)
, if the response has an error, it sets the error’s description on the errorLabel
. The tableFooterView
is set to the errorView
, which contains the errorLabel
. Find these two lines in update(response:)
:
errorLabel.text = error.localizedDescription
tableView.tableFooterView = errorView
Replace them with this:
state = .error(error)
setFooterView()
In setFooterView()
, add a new case for the error
state:
case .error(let error):
errorLabel.text = error.localizedDescription
tableView.tableFooterView = errorView
The view controller no longer needs its error: Error?
property. You can remove it. Inside update(response:)
, you need to remove the reference to the error
property that you just removed:
error = response.error
Once you’ve removed that line, build and run the app.
You’ll see that the loading state still works well. But how do you test the error state? The easiest way is to disconnect your device from the internet; if you’re running the simulator on your Mac, disconnect your Mac from the internet now. This is what you should see when the app tries to load data: