You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

TCA下ForEachStore嵌套SwitchStore时非关联视图意外重渲染问题

Swift Composable Architecture 定时器触发无关视图全量重渲染问题

问题场景

  • AppState中通过IdentifiedArray存储RowState行数据,每个RowState包含EnumRowState枚举类型的状态属性
  • AppView通过ForEachStore渲染RowView,在RowView中通过SwitchStore匹配EnumRowState的枚举值,展示对应类型的子视图
  • 子模块liveReducer中实现了每秒触发一次的Effect.timer,预期定时器带来的状态更新仅会触发对应的LiveView重渲染

异常表现

  • 定时器运行时,无关联状态变更的AddView会同步随定时器每秒重渲染:在AddView中添加的调试控件Text("\(Date())")观测到日期每秒刷新,但AddState本身并未发生任何关联变更
  • 从菜单选择条目后,包括navBarTitle在内的所有视图都会同步更新
  • 为appReducer添加.debug()调试算子后,日志中rows字段的状态变更标注为...(1 unchanged),符合状态未变更的预期,但每次Effect.timer触发时所有行视图仍会全量重渲染

实现说明

中间层reducer参考Swift论坛公开的枚举属性pullback reducer方案编写。

完整复现代码

struct AppState: Equatable {
  var rows: IdentifiedArray<UUID, RowState> = []
}

enum AppAction: Equatable {
  case row(id: UUID, action: RowAction)
}

public struct RowState: Equatable, Identifiable {
  public var enumRowState: EnumRowState
  public let id: UUID
}

public enum EnumRowState: Equatable {
  case add(AddState)
  case live(LiveState)
}

public enum RowAction: Equatable {
  case live(AddAction)
  case add(LiveAction)
}

public struct LiveState: Equatable {
  public var secondsElapsed = 0
}

enum LiveAction: Equatable {
  case onAppear
  case onDisappear
  case timerTicked
}

struct AddState: Equatable { }

enum AddAction: Equatable { }

public let liveReducer = Reducer<LiveState, LiveAction, LiveEnvironment>.init({
  state, action, environment in
  switch action {
  case .onAppear:
    return Effect
      .timer(
        id: state.baby.uid,
        every: 1,
        tolerance: .zero,
        on: environment.mainQueue)
      .map { _ in
        LiveAction.timerTicked
      }
 
 case .timerTicked:
    state.secondsElapsed += 1
    return .none
    
  case .onDisappear:
    return .cancel(id: state.baby.uid)
  }
})

public let addReducer = Reducer<AddState, AddAction, AddEnvironment>.init({
  state, action, environment in
  switch action {}
})

///
/// 用于拉取枚举状态的中间层reducer,供`SwitchStore`使用
///

public var intermediateAddReducer: Reducer<EnumRowState, RowAction, RowEnvironment> {
  return addReducer.pullback(
    state: /EnumRowState.add,
    action: /RowAction.add,
    environment: { ... }
  )
}

public var intermediateLiveReducer: Reducer<EnumRowState, RowAction, RowEnvironment > {
  return liveReducer.pullback(
    state: /EnumRowState.live,
    action: /RowAction.live,
    environment: { ...  }
  )
}

public let rowReducer: Reducer<RowState, RowAction, RowEnvironment> = .combine(
  intermediateAddReducer.pullback(
    state: \RowState.enumRowState,
    action: /RowAction.self,
    environment: { $0 }
  ),
  intermediateLiveReducer.pullback(
    state: \RowState.enumRowState,
    action: /RowAction.self,
    environment: { $0 }
  )
)

let appReducer: Reducer<AppState, AppAction, AppEnvironment> = .combine(
  rowReducer.forEach(
    state: \.rows,
    action: /AppAction.row(id:action:),
    environment: { ...   }
  ),
  .init({ state, action, environment in
    switch action {
    case AppAction.onAppear:
      state.rows = [
          RowState(id: UUID(), enumRowState: .add(AddState())),
          RowState(id: UUID(), enumRowState: .live(LiveState()))
      ]
      return .none
    default:
      return .none
    }
  })
)
.debug()

public struct AppView: View {
  
  let store: Store<AppState, AppAction>
  
  public var body: some View {
      WithViewStore(self.store) { viewStore in
        List {
            ForEachStore(
              self.store.scope(
                state: \AppState.rows,
                action: AppAction.row(id:action:))
             ) { rowViewStore in
               RowView(store: rowViewStore)
             }
          }        
    }
}

public struct RowView: View {
  public let store: Store<RowState, RowAction>
  
  public var body: some View {
      WithViewStore(self.store) { viewStore in
        SwitchStore(self.store.scope(state: \RowState.enumRowState)) {
          CaseLet(state: /EnumRowState.add, action: RowAction.add) { store in
            AddView(store: store)
          }
          CaseLet(state: /EnumRowState.live, action: RowAction.live) { store in
            LiveView(store: store)
          }
        }
      }
  }
}

struct LiveView: View {
  let store: Store<LiveState, LiveAction>

  var body: some View {
    WithViewStore(self.store) { viewStore in
      Text("\(viewStore.secondsElapsed)")
    }
  }
}

struct AddView: View {
  let store: Store<AddState, AddAction>

  var body: some View {
    WithViewStore(self.store) { viewStore in
      // 每次liveReducer的secondsElapsed状态变更时,这里都会触发重渲染!?
      Text("\(Date())")
    }
  }
}

内容的提问来源于stack exchange,提问作者cohen72

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 00:18:20