Open4

TCA: Actionは「〜ButtonTapped」とかつけるのがいいとかいう話だけど本当か?

kabeyakabeya

TCAのチュートリアルがあります。

https://pointfreeco.github.io/swift-composable-architecture/main/tutorials/composablearchitecture/01-01-yourfirstfeature

ここに以下のようなことが書いてあります。

It is best to name the Action cases after literally what the user does in the UI, such as incrementButtonTapped, rather than what logic you want to perform, such as incrementCount.

以下の記事の筆頭にも上がっています。

https://zenn.dev/kalupas226/articles/5b0bf98c922aa0

「何が起こるのかではなく」「ユーザが何をしたのか」という視点は分かるんですけども。
「incrementButtonTapped」はあまりにもUIを限定しぎている気がしませんか。

例えば、音楽プレーヤFeatureを作っていて、playButtonTappedを1回実行するとplay、もう1回実行するとpauseになり、さらにもう1回押すとpauseしたところから再度playされるという感じにしたいと思ったとします。
なんだろう、それってボタンのFeatureであって、音楽プレーヤのFeatureではない、というのかな。だとすると音楽プレーヤFeatureのAction名はもうちょっと違う名前、startPlayとかpausePlayとかにするべきなんじゃないのかなと思ったり。で、これというのはincrementCountとほとんど命名のテイストが同じなんですね。内部では例えば色んなものを同時に変えたりするのかも知れませんが。
あるいは、onStart、onPauseとかdidStart、didPauseとかwillStart、willPauseとかのほうが良いのかとかも思ったり。

kabeyakabeya

Compsableというので、再利用性が高い方がよいと思うんですよね。
〜Tappedとか付けてしまったうえで、別のUIとひっつけると途端にややこしくなります。

kabeyakabeya

Claudeに聞いたところ、以下のようなのはどうかという話でした。
なるほど。入れ子にするということですね。ちょっとしっくりきました。
そしてこの書き方はサンプルにもあるような気がしますね。

Actionの分類:

  • view: ユーザーインタラクションによるアクション
  • delegate: 親コンポーネントへの通知用アクション
  • internal: 内部的な状態更新のアクション
  • child: 子コンポーネントからのアクション

命名パターン:

  • ViewAction: titleChanged, todoTapped など、操作を過去形で表現
  • DelegateAction: didUpdateTodo など、did/will プレフィックスを使用
  • InternalAction: 処理の内容を直接表現(todosLoaded など)
enum Action: Equatable {
        case view(ViewAction)
        case internal(InternalAction)
        case child(ChildAction)
        
        enum ViewAction: Equatable {
            case todoTapped(Todo.ID)
            case addButtonTapped
        }
        
        enum InternalAction: Equatable {
            case todosLoaded([Todo])
        }
        
        enum ChildAction: Equatable {
            case todoDetail(TodoDetail.Action)
        }
    }

この場合、switchのcaseが長くなりますけども。

    switch action {
            case let .view(.todoTapped(id)):
            // ...
            case let .child(.todoDetail(.delegate(.didUpdateTodo(updatedTodo)))):
            // ...
kabeyakabeya

https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.9

に以下のような記述がありました。

Tip
Case key paths offer specialized syntax for many different action types.
PresentationAction’s presented case can be collapsed:

-store.send(.destination(.presented(.tap)))
+store.send(\.destination.tap)

IdentifiedAction and StackAction can be subscripted into:

-store.send(.path(.element(id: 0, action: .tap)))
+store.send(\.path[id: 0].tap)

And BindingActions can dynamically chain into a key path of state:

-store.send(.binding(.set(\.firstName, "Blob")))
+store.send(\.binding.firstName, "Blob")

TestStoreに関する話、ということなんですが、ちょっと深追いしたほうが良さそうな気がします。