Remote Data State as an Enum

Last night I came across How Elm Slays a UI Antipattern article written by Kris Jenkins. In it the author notices that a common list-based UI can be in one of four separate states: notAsked, loading, failure and success and proceeds to model those states explicitly as a sum type.

According to Kris, this approach works well in Elm giving compile-type safety to this common source of UI confusion. Let's see if this approach will fit the stateful world of UIKit by using it for a view based on UITableView.

Before starting I'd also strongly consider using the representation introduced by Scott Hurff in How to fix a bad user interface:

  • Empty State
  • Error State
  • Partial State
  • Loading State
  • Ideal State

For now, though, let's stick with the one presented by Kris. In Swift we'll have the following enumeration:

enum RemoteData<Data, Error: ErrorType> {
    case notAsked
    case loading
    case failure(Error)
    case success(Data)
}

On the UI side we can have two subviews on the same level in the view hierarchy:

  • UITableView
  • PlaceholderView

PlaceholderView is a simple view with only title property for a demonstration purposes – but we can easily add more information, like an image in the future:

class PlaceholderView: UIView {

    struct Configuration {
        let title: String
    }

    @IBOutlet weak var titleLabel: UILabel?
    var configuration: Configuration?

    ...
}

As you can see it has Configuration struct encapsulating its configuration. Since working with a raw enum isn't pleasant we can create a helper struct called UserInterfaceState:

struct UserInterfaceState<Data, Error: ErrorType> {
    let remoteData: RemoteData<Data, Error>

    var viewWithDataShouldBeHidden: Bool {
        return placeholderConfiguration() != nil
    }

    var placeholderViewShouldBeHidden: Bool {
        return !viewWithDataShouldBeHidden
    }

    var data: Data? {
        if case .success(let data) = self.remoteData {
            return data
        } else {
            return nil
        }
    }

    init(remoteData: RemoteData<Data, Error>) {
        self.remoteData = remoteData
    }

    func placeholderConfiguration() -> PlaceholderView.Configuration? {
        switch self.remoteData {
        case .notAsked:
            return PlaceholderView.Configuration(title: NSLocalizedString("View is waiting for an update", comment: "notAsked state"))
        case .loading:
            return PlaceholderView.Configuration(title: NSLocalizedString("Loading", comment: "loading state"))
        case .failure(let error):
            return PlaceholderView.Configuration(title: NSLocalizedString("Error happened: \(error)", comment: "failure state"))
        case .success(_):
            return nil
        }
    }
}

Each time we get a new instance of RemoteData we create a new instance of UserInterfaceState and update our UI accordingly, e.g.:

tableView?.hidden = state.viewWithDataShouldBeHidden
dataSource.data = state.data

placeholderView?.configuration = state.placeholderConfiguration()
placeholderView?.hidden = state.placeholderViewShouldBeHidden

Even though we omitted many details here, I think it's clearly visible that with this technique we can achieve a better UX with not that much additional work on our part.