Reasons for the view not to rerender

I have a state that has a bool and an optional array.

struct ThingListState: Equatable {
    var flag: Bool = true
    var things: [Thing]? = nil
}

During the view's life cycle the boolean flips and optional array (that was nil) turns to an empty array.

state.flag = false
state.things = []

And after these mutations I return Effect.none from the reducer.

The problem is that the view doesn't get re-rendered after the state change. All of the rendition logic is inside WithViewStore.

Using the .debug() on my main reducer I can see the following sequence of actions and state changes:

[Things]: Evaluating WithViewStore<ThingListState, ThingListAction, ...>.body
received action:
  ThingListAction.loadThings
  (No state changes)

[Things]: Evaluating WithViewStore<ThingListState, ThingListAction, ...>.body
received action:
  ThingListAction.loadedThings(
    [
    ]
  )
  ThingListState(
−   things: nil,
−   flag: true,
+   things: [
+   ],
+   flag: false
  )

[Things]: Evaluating WithViewStore<ThingListState, ThingListAction, ...>.body

The [Things] marker is placed as a debug() view modifier for WithViewStore(store) container. So I can see that the re-rendering is being requested, however the content on the screen doesn't change.

The simplified version of the ThingListView is following:

struct ThingListView: View {

    var store: Store<ThingListState, ThingListAction>

    var body: some View {
        WithViewStore(store) { viewStore in
            if viewStore.flag {
                Text("Loading...")
            } else if viewStore.thingStates.isEmpty {
                Text("No things.")
            } else {
            	// do the `ForEachState` with `thingStates`
            	// omitted for brevity
            }
        }
    }
}

Am I missing something trivial?

Have you found a solution for this issue?

It may be related to SwiftUI's state diffing, where it only re-renders the view if it thinks that the state changed in a meaningful way. Maybe a nil optional array and an empty array occupy the exact same memory – therefore SwiftUI's use of memcmp(), which directly compares two blocks of memory, doesn't see any difference between the two states.

Maybe verify if you get the same issue with a vanilla SwiftUI implementation to see if this is a problem with TCA or with SwiftUI in general.

Can you provide a small sample demonstrating the issue?

I am unable to figure out where thingsStates is defined. Is it a computed property on ThingListState?

This works on my end... but I may be missing something from your example.

import Combine
import ComposableArchitecture
import SwiftUI

struct Thing: Equatable, Identifiable {
  var id: String
}

struct ThingListState: Equatable {
  var isLoaded: Bool = false
  var things: [Thing]? = nil
  var thingStates: [Thing] { things ?? [] }
}

enum ThingListAction {
  case loadThings
  case loadedThings([Thing])
}

let thingListReducer = Reducer<ThingListState, ThingListAction, Void> { state, action, _ in
  switch action {
  case .loadThings:
    let things = [Thing]()
//    let things = Array(0 ..< 5).map { Thing(id: "\($0)") }
    return Effect(value: .loadedThings(things))
      .delay(for: 1, scheduler: DispatchQueue.main)
      .eraseToEffect()
  case .loadedThings(let things):
    state.isLoaded = true
    state.things = things
    return .none
  }
}

struct ThingsListView: View {
  let store: Store<ThingListState, ThingListAction>

  var body: some View {
    WithViewStore(store) { viewStore in
      VStack {
        if !viewStore.isLoaded {
          Text("Loading...")
        } else if viewStore.thingStates.isEmpty {
          Text("No things.")
        } else {
          ForEach(viewStore.thingStates) { Text($0.id) }
        }
      }.onAppear { viewStore.send(.loadThings) }
    }
  }
}

struct ThingsListView_Previews: PreviewProvider {
  static var previews: some View {
    ThingsListView(store: .init(
      initialState: .init(),
      reducer: thingListReducer,
      environment: ())
    )
  }
}

I haven't found a solution, but I started implementing conditional views using IfLetStore instead of simple conditions/flags in the state :)

Awesome. That would have been my next recommendation :)

You could abstract your value states into an enum:

enum Loadable<Value> {
  case none
  case loading(previous: Value?)
  case error(Error, previous: Value?)
  case some(Value)

  var isLoading: Bool {
    guard case .loading = self else { 
      return false
    }

    return true
  }
}

and then switch over a Loadable<IdentifiedArrayOf>. The four states cover most UI states.

struct ThingsListView: View {
  let store: Store<ThingListState, ThingListAction>

  var body: some View {
    WithViewStore(store) { viewStore in
      Group {
        if viewStore.list.isLoading {
          ActivityIndicator()
        }

        switch(viewStore.list) {
          case .none, .loading(.none):
            EmptyView()
          case .loading(.some(let previous)), .error(_, .some(let previous)):
            List(for: previous)
          case .some(values):
            List(for: values)
        }
      }
      .onAppear { viewStore.send(.loadThings) }
    }
  }
}