Swift TCAでStateの変更を検知して自動でActionを発行する方法

# 現状

[isowords](https://github.com/pointfreeco/isowords/blob/main/Sources/TcaHelpers/OnChange.swift)の`OnChange`を使っている。


```swift
var body: some ReducerProtocol<State, Action> {
    core
        .onChange(of: \.errorMessage) { message, _, _ in
            if message == nil {
                return .none
            } else {
                return .run { send in
                    await send(.setErrorMessageNil)
                }
            }
        }
}

@ReducerBuilderOf<AppCore>
var core: some ReducerProtocol<State, Action> {
    Reduce { state, action in
    // ...
    }
}
```

# 初期

TCAではStateが変わらないとVCにStateが流れてこないので、同じエラーメッセージを出したい時などは一回エラーメッセージにnilをセットする必要があります。

TCAよく分かってなかった初期の頃は（今も特に分かってない）、絶対もっと良い方法あるだろうなーと思いつつVCでアラートのボタンがタップされた時に`setErrorMessageNil`のようなActionを発行していました。


```swift
struct AppCore: ReducerProtocol {
    struct State: Equatable {
        var errorMessage: String?
    }

    enum Action: Equatable {
        case setErrorMessageNil
    }

    var body: some ReducerProtocol<State, Action> {
        switch action {
            case .setErrorMessageNil:
                state.errorMessage = nil
                return .none        
        }
	}
}
```

#### ViewController
```swift
/// アラートのOKがタップされた時に呼ぶメソッド
func didTapOk() {
    store.send(.setErrorMessageNil)
}
```

# 中期

何か良い方法がないかとサンプルを探してたら[Example](https://github.com/pointfreeco/swift-composable-architecture/blob/main/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-Animations.swift#L58)に下記のようなコードがありました。

```swift
case .rainbowButtonTapped:
  return .run { send in
    for color in [Color.red, .blue, .green, .orange, .pink, .purple, .yellow, .black] {
      await send(.setColor(color), animation: .linear)
      try await self.clock.sleep(for: .seconds(1))
    }
  }
  .cancellable(id: CancelID.self)
```

こういう書き方もできるのかーと思って早速導入しました。


```swift
struct AppCore: ReducerProtocol {
    struct State: Equatable {
        var errorMessage: String?
    }

    enum Action: Equatable {
        case apiResponse(Result<APIResponse, CustomError>)
        case setErrorMessageNil
    }

    var body: some ReducerProtocol<State, Action> {
        switch action {
            case .apiResponse(.failure(let error)):
                state.errorMessage = error.description
                return .run { send in
                    await send(.setErrorMessageNil)                    
                }
            case .setErrorMessageNil:
                state.errorMessage = nil
                return .none        
        }
	}
}
```

VC上でActionを発行する必要が無くなりコードがシンプルになりました。


# 今

Actionが増えてerrorMessageを処理する箇所が増えたり、他のStateが増えていくにつれて「毎回`.run`書くの面倒だな...」と思い始めまたサンプルを探していたら[OnChange](https://github.com/pointfreeco/isowords/blob/main/Sources/TcaHelpers/OnChange.swift)を発見しました。

[使ってる箇所](https://github.com/pointfreeco/isowords/blob/bb0a73d20495ca95167a01eeaaf591a540120ce2/Sources/GameCore/SoundsCore.swift#L23)


```swift
.onChange(of: { $0.gameOver == nil }) { _, _, _ in
  .fireAndForget {
    await Task.cancel(id: CubeShakingID.self)
    for music in AudioPlayerClient.Sound.allMusic where music != .gameOverMusicLoop {
      await self.audioPlayer.stop(music)
    }
  }
}
```

最初は単純にonChangeでActionを発行していたのですが、nilチェックしないとActionが複数回発行されてしまいテスト書く時に想定してない挙動をするので注意です（やりました）


```swift
// ❌ これだとStateにnilがセットされた時にまたonChangeが呼ばれて無駄にActionが発行されることになる
.onChange(of: \.errorMessage) { _, _, _ in
	return .run { send in
        await send(.setErrorMessageNil)				
	}
}

// ⭕　nilチェックをすれば無駄なactionが発行されない
.onChange(of: \.errorMessage) { message, _, _ in
    if message == nil {
        return .none
    } else {
        return .run { send in
            await send(.setErrorMessageNil)
        }
    }
}
```

[ライセンス](https://github.com/pointfreeco/isowords/blob/main/LICENSE.md)的にどうなのかと思ってDeepLで翻訳してみたら

- ソフトウェア全体（isowordsアプリ自体）を複製、配布できるのは非商用の学習、開発、研究、教育目的のみ
- コードの一部であれば競合でない限り商用利用可能

というふうに読み取れたのでisowords的なパズルゲームを作らない限り商用利用でも使って問題なさそうな気がするけどどうでしょう。

ライセンスに詳しい方。

コメントください。


# TCA使ってみて

## pros

- テストが書きやすい。
想定してないアクションが発行されてる、Effect(API)の呼び出しが完了してないなどがテストで検知できるから便利だなーって思いました。

## cons

- 情報が少なくて正しい書き方、プラクティスみたいなのがいまいち分からない。(Swift TCAのサンプルコード探しまくることになる）
- 困った時にググってブログ記事やStack Overflowで解決...みたいなことがあまりできない。
- 1.0に達してないのでインターフェースがまた変わることがあるかもしれない（？）