Galapagos Tech Blog

株式会社ガラパゴスのメンバーによる技術ブログです。

「TCAつかってみます」に参加します

AIR Design for Apps 事業部 アプリエンジニアチーム iOSユニットの亀澤です。
試用期間を終えて正式に社員になりました。

さて、突然ですが皆さんはiOSのプロジェクトでアーキテクチャを採用していますか? MVCとかMVVMとかVIPERとか、聞いたことあるけど本格的に始めるというほどではなかったのですが、今回 the Composable Architecture(以降TCA )を採用したプロジェクトに参加することとなりました。 転職前は自社サービス、それも古くからサービスを行っているアプリケーションが多かったので、アーキテクチャをやっと取り入れようとしていたり、SwiftUIも使う機会がなかなかありませんでした。以下今回TCA+SwiftUIを採用したプロジェクトに初参加した感想です。

TCA採用の経緯はこちら

techblog.glpgs.com

さらっと解説を見た感じでは

  • SwiftUIとの親和性が高い
    今回はSwiftUIですがUIKitもサポート
  • テストが書きやすい
    画面や外部に依存する部分を予め考えて作るのでロジック部分のテストが書きやすい アクションを送ったらちゃんとstateが変わるかの確認ができればviewと分離されているのでロジックの担保がしやすい

ふむふむ、今どきな感じですね。あとはやってみて慣れるしかないかなと飛び込んでみました。

始めてみるとやっぱり読んだだけでは理解できてないですね、色々とハマりました。

最初に作ったのはこんな感じ

AppがあってHomeが表示されていてそこからモーダル画面の中にある詳細表示のViewにあるパーツのView(赤いView)を追加します。 緑がアプリケーション、灰色の枠はViewControllerでその他がViewと思って下さい。 それぞれの枠にStore(State,Action,Reducer)があります。

  • ついViewやStateに書いてしまう
    表示したViewサイズの画像をAPI経由で取得したときに、表示されたタイミングでそのまま受け取った情報を反映してしまったり。 Stateを初期化の時に色々加工してしまって後々テストデータを注入できなかったり 初めて作ったときだったのでStoreを作るほどでもないかとViewだけで作ったのですが、ここだけTCAに対応しないViewにしてしました。 地図を表示するためにAPIを使ったのですが、OnApprearでViewにOnAppaerでAPIからURLから画像を更新しています。 URLが状態ですのでStateに入れてonAppearでアクションを送りreducerでAPIを使いURLを更新して画像を読み込むのが正解ですね。

今度はちゃんとView・State・Reducerを持つViewのパーツを作りました。

struct PartsStore {
    static let reducerCore = Reducer<State, Action, Environment> { state, action, environment in
        switch action {
        case .tapButton:
            return .none
        }
    }
}

enum PartsAction: Equatable {
    case tapButton
}

struct PartsView: View {
    var body: some View {
        Button(action: { partsStore.send(.tapButton) },
                     label: { Text("button"} )
    }
}
  • どこのreducerで処理したらいいのか迷う
    最初に作ったのはモーダル画面に貼られる地図表示のViewなのですが、Actionをどこで拾うかで迷いました。
    作ったViewのStateとReducerの他に、貼り付けられているViewのReducerとアプリケーション元のReducerが出てきます。
struct TopState {
    static let reducerCore = Reducer<State, Action, Environment> { state, action, environment in
        switch action {
        case  PartsAction(.tapButton): 
            return .none
        }
    }
}

struct ParentState {
    static let reducerCore = Reducer<State, Action, Environment> { state, action, environment in
        switch action {
        case PartsAction(.tapButton):
                return .none
        }
    }
}

受け取ったActionを自分のreducerから上のreducerにActionを変えて送るのが正しいのか、そもそも受け取るのが自身のReducerなのか、親のReducerなのか、さらにその元となっているReducerなのかとか混乱しました。画面遷移をするのでパーツである自分の Actionをリレーしていく仕組みが理解できるまで苦労しました。

  • イベントが反応しない Viewで発生したイベントをそのStore単体内で処理するのは良いのですが子画面、孫画面となった場合reducerもツリーにしてイベントを渡していかないとアクションが届かないし、作った画面内では動いていたのにいざ画面を連携させてみたらイベント飛んでこないとか 親画面にアクションを繋げたくて親画面で作ったActionを使ってしまったりとイベントチェーンのつながりが慣れるまで辛かったですね。 上のコードで省略していますが、親が子へアクションがつながるように子を作る際に作ります。作り忘れるとアクションが届かないので上から順番に届いているか見てみると途中で途切れていたりします。
  • テストがしにくい
    経験不足からいまいちな実装してしまいテストできるようにするのに困りました。 正確にはテストが「しにくい」じゃなくて「しにくくしていた」のは自分だったのですが、ちゃんとDIすべき所はするようにしないとダメですね。

とハマったところばかりで悪い感じに書きましたがいいところもあります。

  • テストしやすい
    先ほどと矛盾していますがちゃんと書いていれば enviromentでDIする環境が整っているので、アクションを送信してStateがどう変わったかを見れば良いし、 画面はSwiftUIで作っているので既に用意してあるテストデータをツッコめば確認できます。 あるアクションを送ってその処理でさらに次のActionを送っている場合、次に受信しているActionを拾っていないと警告出るので取りこぼしもないです。
  • 書きにくいときは書くところを間違えている
    なんとなく何かの処理を書こうとして詰まる、必要なデータにアクセスできなかったり、そんな時には書く場所を間違えています。 一つずつきちんと書ければ難しいことはないのですが、つい余計なことをしようとすると急にわけがわからなくなります。 あれ?!と思ったら適切にしてなかったと気付けます。

なかなか慣れずに苦労しましたが、慣れてくるとなかなか楽しいです。 ためしにTCAを触ってみるのもいかがでしょうか?