UIViewRepresentableなビューにTCAを組み込むべきか?
まだSwiftUIにはUITextView相当のものが用意されていないので、UIViewRepresentableを使ってUITextViewをSwiftUIビューで使用できるようにしています。
さらに、自分が関わっているプロジェクトではThe Composable Architecture(TCA)を採用しているのですが、このビューにTCAを組み込むべきかどうかで悩みました。
つまり、@Binding等を使ってピュアSwiftUIビューとするか、State/Action/Reducerを組み込んでTCAのコンポーネントとするかどうかです。
結論から言うと、ピュアSwiftUIビューでいくことにしました。
本記事ではなぜピュアSwiftUIビューにすることにしたかを説明したいと思います。
TCAで実装できないこともないが、一貫性のない実装になってしまう
例えばTCAで実装するとこんな感じになるかと思います(必要なとこだけ抜粋)。
struct CustomTextView: UIViewRepresentable { private let viewStore: ViewStore<CustomTextState, CustomTextAction> private let textView = UITextView() init() { ... viewStore.publisher.backgroundColor .sink { self.textView.background = $0 // Escaping closure captures mutating 'self' parameter } .store(in: &cancellables) } func makeCoordinator() -> Coordinator { Coordinator(viewStore: viewStore) } class Coordinator: NSObject, UITextViewDelegate { func textViewDidChange(_ textView: UITextView) { viewStore.send(.textViewDidChange(newText: textView.text, contentHeight: textView.contentSize.height)) } }
UITextViewDelegateのメソッドではActionを送信するだけにして、ロジックをReducerにまとめてます。
こうするとテストが可能になるので、良い感じだなあと思っていました。
ところが、イニシャライザで実装しているようにStateの変更をトリガーにUITextViewのプロパティを変更したいと思っても、Escaping closure captures mutating 'self' parameter
というエラーが出てコンパイルできません。
CustomTextViewがstructだからですが、ここをclassにすることはできないので、このような実装はできません。
Delegateメソッド内であればプロパティ変更をすることはできますが、一部はReducer、一部はDelegateメソッドという感じでロジックが散らばるのは実装に一貫性がなくあまりやりたくありません。
TCAの作者がピュアSwiftUIビューにすることを推奨していた
同じような悩みを抱えてる人いないかなーとTCAのGithub Discussionsを覗いてみたら、TCAの作者がこういうコメントをしていました
Anytime I've had to deal with view representables I've kept them as a plain SwiftUI entity using bindings and then passed in TCA derived bindings from the outside. This may not work with really complex view representables, but it's a good idea to start there.
UIViewRepresentableなビューではTCAは使わず、プレーンなSwiftUIビューにしたほうが良いとのこと。
もうちょっと具体的な理由が欲しかったですが、作者がこう言っているので、私もそれに従おうと思いました。そもそも上述したように、思うような実装もできませんので。