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.