onevcat / OneV-s-Den-Comments

0 stars 0 forks source link

2022/03/tca-3/ #33

Open utterances-bot opened 2 years ago

utterances-bot commented 2 years ago

TCA - SwiftUI 的救星?(三) | OneV's Den

在上一篇关于 TCA 的文章中,我们看到了绑定的工作方式以及 Environment 在管理依赖和提供易测试性时发挥的作用。在这篇文章中,我们会继续深入,来看看 TCA 中的两个重要话题:Effect 角色到底是什么,以及如何通过组合的方式来把多个小 Feature 组合在一起,形成更加

https://onevcat.com/2022/03/tca-3/

yongyang007 commented 2 years ago

EmptyView().onAppear() 好像并没有执行,请问这是SwiftUI的问题么?

onevcat commented 2 years ago

@yongyang007 啊,我疏忽了,EmptyView 的 modifier 的话确实是什么都不做的。我修改一下...

lingfengmarskey commented 2 years ago

puulback -> pullback

RayJiang16 commented 2 years ago

如果大组件中包含小组件的数组,这种情况下大组件的 reduce 是不是无法用 pullback 拉回小组件的 state 呢?需要怎么样同步小组件的 state 呢?

onevcat commented 2 years ago

@RayJiang16 这方面也许在下一篇文章里有一些提及?https://onevcat.com/2022/05/tca-4/

Khala-wan commented 2 years ago

赞!请问有办法对 ViewStore.send(action) 进行类似 Throttle 的操作吗?比如一个 TextField 输入的 Text 可能会非常频繁地 sendAction,但我只想每 0.3 秒获取一次输入的信息发起网络请求。

onevcat commented 2 years ago

@Khala-wan 我的想法是直接在 UI 层做一个 PasstgroughSubject 之类的东西,然后直接用 Combine 的 throttle 应该就行了吧。

Khala-wan commented 2 years ago

确实是个解决方案,但是就能不能用 TextField(text: viewStore.binding(\searchText, send: .searchTextChanged)) 这种方式了。需要走从 UI 层走一遍数据转换, 看起来可能有点繁琐。这里有 https://forums.swift.org/t/issues-with-throttling-and-potential-solutions/36812 (另一种更 TCA 的解法)。不过这个方式我在iOS 13 上中了 Combine 的 BadAccess。

andy1413 commented 1 year ago

@onevcat 我跑了一下demo代码,发现每点击一次+号,CounterView body会调用两次,我理解是子组件state变更刷新了一次,父组件state变更又刷新了一次,这里会有问题,如果嵌套层级很多,很多子组件会刷新很多次,这里是否考虑使用class代替struct不在子组件和父组件之间传递Action和State会解决刷新问题?

onevcat commented 1 year ago

@wangfangshuai 这是有可能的:ViewStore 就是让当前 view 订阅某个 store,“默认”情况下,这个 store 里的 state 的所有属性都会被订阅,于是造成性能问题。最近版本的 TCA 为创建 ViewStore 时提供了额外的 observe 参数,来限定父 view 需要订阅的属性。具体的建议参考一下 Performance 的相关文档 以及这里的讨论

onevcat commented 1 year ago

这里是否考虑使用class代替struct不在子组件和父组件之间传递Action和State会解决刷新问题?

完全不推荐这种做法,而且可能这么做会让整个 Publisher 链挂掉。对于 State 角色,尽量避免使用引用语义。

andy1413 commented 1 year ago

@onevcat 我尝试根据 Performance 的相关文档 的说明进行修改:

struct GameView: View {
  let store: Store<GameState, GameAction>

  struct ViewState: Equatable {}

  var body: some View {
      WithViewStore(store.stateless, observe: ViewState.init) { viewStore in
          VStack {
            TimerLabelView(store: store.scope(state: \.timer, action: GameAction.timer))
            CounterView(store: store.scope(state: \.counter, action: GameAction.counter))
          }.onAppear {
            viewStore.send(.timer(.start))
          }
      }
  }
}

添加observe,初始化一个空的ViewState传入,但是依然会调用两次CounterView的body,你能修改一下demo,让CounterView的body可以调用一次吗?谢谢! Performance 的相关文档的文档并不太容易理解。

onevcat commented 1 year ago

你是用什么方式来确认 body 的调用的?我这边尝试了一下,Button 似乎只会触发一次 body,应该是按预期工作的。

andy1413 commented 1 year ago
struct CounterView: View {
  let store: Store<Counter, CounterAction>
  var body: some View {
    WithViewStore(store) { viewStore in
      VStack {
        checkLabel(with: viewStore.checkResult)
        HStack {
          Button("-") { viewStore.send(.decrement) }
          TextField(
            viewStore.countString,
            text: viewStore.binding(
              get: \.countString,
              send: CounterAction.setCount
            )
          )
            .frame(width: 40)
            .multilineTextAlignment(.center)
            .foregroundColor(colorOfCount(viewStore.count))
          Button("+") { viewStore.send(.increment) }
        }
        Slider(value: viewStore.binding(get: \.countFloat, send: CounterAction.slidingCount), in: -100...100)
        Button("Next") { viewStore.send(.playNext) }
      }.frame(width: 150)
    }
  }

断点打在VStack {那一行发现断点会走两次,但是断点打在checkLabel(with: viewStore.checkResult)那一行发现断点只会走一次,看起来第二次只是调用了一下,没有实际显示,所以没有触发checkLabel代码的执行,看起来问题不大。我仔细想了一下,store.scope(state: .counter内部如果是copy struct couter的话,所以实际触发CounterView刷新的就只是CounterView持有的store: Store<Counter, CounterAction>里边的Counter struct,跟父试图的无关,修改父视图的store里的counter struct应该确实不会对子视图产生刷新作用。