Enums are an excellent way to leverage Swift’s value-types and immutability principles for handling states. Imagine you have a view that fetches a list of items from an API. You can represent that view’s state as the following enum:

enum ViewState {
  case idle,          // next state: .loading
       loading,       // next states: .items or .error
       items([Item]), // next state: .loading
       error(Error)   // next state: .loading
}

Your view is idle before fetching your list. When it does so, you update your view’s state to .loading and add an appropriate indicator. From there you’ll either have a successful state with the list you just loaded, or an error, much like a Result.

Now if your View, or ViewModel has a ViewState attribute, how would you render that in SwiftUI?

struct ItemsListView: View {
  @State var state: ViewState = .idle

  var body: some View {
    // TODO: render `state`
  }
}

You could add a switch-case to your view’s body, but that’d be mixing imperative programming with the SwiftUI’s declarative syntax. It works, but it’s less than ideal and just ugly, in my honest opinion.

You could also add a method that returns a View for the given state. Something like:

struct ItemsListView: View {
  @State var state: ViewState = .idle

  var body: some View {
    stateView()
  }

  private func stateView() -> some View {
    switch state {
      case .idle: return EmptyView().eraseToAnyView()
      case .loading: return Spinner().eraseToAnyView()
      case .items(let list): return ItemsList(list).eraseToAnyView()
      case .error(let error): return ErrorView(error).eraseToAnyView()
    }
  }
}

Do yourself a favor and add the much needed modifier that wraps a View into AnyView and erases its type, making it semantically consistent to Combine’s eraseToAnyPublisher()

This is better, but still not great since you need to refer to code outside your View’s body to know what is being rendered. Don’t get me wrong: componentizing is great when you’re grouping views into containers like “header”, “footer”, “hero”, “left panel”, but you should only group components that make sense to be grouped. States are not containers. They represent different possibilities and, therefore, shouldn’t be grouped. So how do we fix that in order to have our state being rendered within our View’s body? The answer is to make ViewState provide the correct view:

private extension ViewState {
  func render<Idle: View, Loading: View, Items: View, ErrorView: View>(
    idle: () -> Idle,
    loading: () -> Loading,
    items: ([Item]) -> Items,
    error: (Error) -> ErrorView
  ) -> some View {
    switch self {
      case .idle: return idle().eraseToAnyView()
      case .loading: return loading().eraseToAnyView()
      case .items(let list): return items(list).eraseToAnyView()
      case .error(let err): return error(err).eraseToAnyView()
    }
  }
}

Make sure to add this private extension in your View’s file, not where you declared the ViewState enum. You can also wrap any of render’s arguments in @ViewBuilder if it makes sense for your use-case.

Now all we have to do is call render in our View’s body:

struct ItemsListView: View {
  @State var state: ViewState = .idle

  var body: some View {
    state.render(
      idle: { EmptyView() },
      loading: { Spinner() },
      items: { list in
        ItemsList(list)
      },
      error: { error in
        ErrorView(error)
      }
    )
  }
}

// ViewState's private extension can go here

Isn’t that much better? Our View’s organization is on-point by keeping things where they should be, while still leveraging what Swift offers best (immutability, switch-cases exhaustiveness, and generics). That’s the pattern I’ve being going for with great success in both SwiftUI apps I’ve implemented so far.

Bonus

If you want to add that AnyView modifier I mentioned above, here’s the code:

extension View {
  func eraseToAnyView() -> AnyView {
    AnyView(self)
  }
}

Using native and non-native animations together

React-Native animations has some limitations on what can be done using the native driver and whatcan only be executed in the Javascript …… Continue reading

Rogue Bit 🕹

Published on October 31, 2019

The Art of Coding

Published on September 26, 2019