Reduce bugs with a UI state enum in Swift & UIKit
Tags: ios, swift, uikit, patterns
Published: 2021-04-06 00:42:56

Keeping track of UI state on iOS can be a tricky problem, at least when using UIKit. Apple is making huge strides with SwiftUI to create a more modern declarative UI framework similar to React or Flutter, but many iOS developers can't use it on their existing apps until more users upgrade their devices. Until then, we can implement a pattern using Enums to reduce bugs in views which have multiple different states they can be in.

Suppose we have a Search view controller that contains a `UISearchBar`. In order for us to get notified when the user taps the cancel button, we can conform our view controller to `UISearchBarDelegate`, which will call our method `searchBarCancelButtonClicked(_ searchBar: UISearchBar)` when the cancel button is tapped.

Tapping the cancel button needs to trigger quite a few actions. First we clear the SearchBar text by setting it to nil, second we tell the SearchBar to hide the keyboard since the user is done searching, third we tell the contained `SearchTableVC` to clear its search results by sending `nil` as a new query, and finally we tell our delegate (super view / view controller) that it should hide its search UI.

```swift
extension SearchVC: UISearchBarDelegate {

    func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
        searchBar.text = nil              // 1
        searchBar.resignFirstResponder()  // 2
        searchTableVC.search(query: nil)  // 3
        delegate.hideSearch()             // 4
    }
}
```

This is all understandable right here, in this one method, but there are many other places in the Search view controller where we need to update the UI state, and it's incredibly easy to forget to update some bit of the UI and get ourselves in an inconsistent and buggy state.

What if our next task is to add a `UIActivityIndicatorView` spinner? Can we remember all of the places in our code to add commands to start and hide the spinner?

There is a better way. Let's define an `enum` that will store each discrete state the app can be in:

```swift
enum SearchState {
    case hidden
    case emptyView
    case startSearch(String?)
    case showingResults
}
```

The search view can be in only one of these states at a time: 

1. Totally hidden
2. Visible, but empty (no results) with either no query or a partial search query entered
3. Loading search results (`startSearch(String?)`), passing search query along
4. Showing search results

Then we add a `state` variable to hold the state this view controller is in. Every time `state` changes we will update the needed views; everything is all in one place which makes it much easier to reason about and much harder to miss something.

```swift
private var state: SearchState = .emptyView {
    didSet {
        switch state {
        case .hidden:
            activitySpinner.stopAnimating()
            searchBar.text = nil
            searchBar.resignFirstResponder()
            searchTableVC.search(query: nil)
            delegate.hideSearch()
            
        case .emptyView:
            activitySpinner.stopAnimating()
            searchBar.text = nil
            searchBar.becomeFirstResponder()
            searchTableVC.search(query: nil)
            
        case .startSearch(let query):
            activitySpinner.startAnimating()
            searchBar.resignFirstResponder()
            searchTableVC.search(query: query)
            
        case .showingResults:
            activitySpinner.stopAnimating()
        }
    }
}
```

Finally in our `searchBarCancelButtonClicked(_ searchBar: UISearchBar)` method, instead of four lines that update various UI directly, we just set the state like this:

```swift
extension SearchVC: UISearchBarDelegate {

    func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
        state = .hidden
    }
}
```

And when the search button on the keyboard is clicked, we can set `state` to `.startSearch()`, passing the search bar text as the search query.

```swift
extension SearchVC: UISearchBarDelegate {
    
    func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
        state = .startSearch(searchBar.searchTextField.text)
    }
}
```

That's it! State management can be a complex topic, and there are many ways to approach it, including large libraries you can import to your project, but something fairly simple like this `enum` method helps a lot to keep your view logic organized and working correctly.