LoginSignup
1
1

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-12-21

現状

isowordsOnChangeを使っている。

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を発行していました。

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

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

中期

何か良い方法がないかとサンプルを探してたらExampleに下記のようなコードがありました。

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)

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

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を発見しました。

使ってる箇所

.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が複数回発行されてしまいテスト書く時に想定してない挙動をするので注意です(やりました)

// ❌ これだと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)
        }
    }
}

ライセンス的にどうなのかと思ってDeepLで翻訳してみたら

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

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

ライセンスに詳しい方。

コメントください。

TCA使ってみて

pros

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

cons

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1