DEV Community

loading...

Reduce bugs with a UI state enum in Swift & UIKit

chadparker profile image Chad Parker ・3 min read

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.

extension SearchVC: UISearchBarDelegate {

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

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:

enum SearchState {
    case hidden
    case emptyView
    case startSearch(String?)
    case showingResults
}
Enter fullscreen mode Exit fullscreen mode

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.

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()
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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

extension SearchVC: UISearchBarDelegate {

    func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
        state = .hidden
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

extension SearchVC: UISearchBarDelegate {

    func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
        state = .startSearch(searchBar.searchTextField.text)
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (0)

pic
Editor guide