はじめに
先月、 【「SwiftUIでMVVMを採用するのは止めよう」と思い至った理由】
という記事を公開し、多くの反響がありました。
上記の記事では
「じゃあ、MVVMをやめて、アーキテクチャは何を採用すればよいの?」
という問いに対する、明確な答えを出していませんでした。
あれから時が経ち、今ならば、この問いに対して、
ぼくは 「The Composable Architecture(TCA)をおすすめします」 と答えることができます。
以下は公式ページから抜粋したものを翻訳しました。
「The Composable Architecture(TCA)」の目的について、以下の様に記述されています。
アプリケーションアーキテクチャの探求は、どんなアーキテクチャも解決することを目的とする核となる問題を理解することから始めます。そして、SwiftUIが状態管理にアプローチする方法を見ることで、これらの問題を探求します。これは、解決しなければならない5つの大きな問題を定式化することにつながり、この時点から私たちのアーキテクチャの開発を導きます。
TCAが解決する5つの大きな問題とは、以下のように記述されています。
・アプリケーション全体の状態を管理する方法
・値の型のような単純な単位でアーキテクチャをモデル化する方法
・アプリケーションの各機能をモジュール化する。
・アプリケーションの副作用をモデル化する。
・各機能の包括的なテストを簡単に記述する方法
現在、ぼくは、TCAを実際に仕事で使っていて、上記の5つの問題に頭を悩ませていたので、 「これはいいものだ」 という確信を持っています。
一方で、TCAには、 学習コストの高さや、ライブラリの開発体制が不安(※個人の感想です) というデメリットがあります。
ですが、デメリットはいったんは脇において、 メリットを享受するほうが大きい という判断に至りました。
本記事の内容は、あくまで、現在、「私はこう考えた」という意味であって、「この考えが絶対的に正しい」という主張ではありません。
人によって、見ているコードベースや、バックグラウンド、考え方、観点、解釈の違いによって、そのアーキテクチャの選択がよいのかどうか、様々だと思います。
時と場所が違えば、私はTCAの使用をやめて、MVVMに舞い戻るということが無きにしもあらずですから。
(現在、ぼくは複数の仕事をしていて、ある会社では、MVVMでバリバリViewModelを書きまくっています)
そもそも小中規模なアプリであれば、TCAを採用するのは、「大げさすぎるかなぁ」 と思います。
本記事では、MVVMとTCAのコードを比較しながら、MVVMをやめて、TCAを採用するメリット/デメリットについて、詳しく解説します。
実際に、あなたの現場で、TCAを導入することが本当によいとは限らないので、この記事に書いてある内容を鵜呑みにしないで、よく考えてアーキテクチャを採用することをおすすめします。
もしこの記事が、同じように MVVMから別のアーキテクチャに移行を考えている人 の参考になれば幸いです。
本記事で説明するコードは、以下のリポジトリに公開しています。
https://github.com/karamage/swiftui-with-mvvm-to-tca-counter
「The Composable Architecture(TCA)」を採用する「理由」
本記事は、SwiftUIでMVVMをやめて「The Composable Architecture(TCA)」を採用する 「理由」を 説明します。
一方で、TCAの詳しい使い方の説明は、省略させていただきます。
TCAの使い方や詳細な説明が知りたい方は、以下の記事やドキュメントが参考になりましたので、そちらを御覧ください。
宣言的UIの登場で、UIのコンポーネント化(部品化)がすすむ
SwiftUIの登場で、状態管理とデータフローがより重要 になってきています。
なぜかというと、SwiftUIで、UIの コンポーネント化(部品化)が容易 になるためです。
SwiftUI/Jetpack Compose/ReactNative/Flutterなどの、宣言的UIを使うと、コンポーネントをより簡潔に部品化しやすくなります。
しかしながら、UIの部品化が進むと、今度は、その 部品を組み合わせて画面を構成する(Compose) という作業が必要になってきます。
それはあたかも、レゴブロックで、お城を組み上げる作業に似ています。
部品化されたUIを、組み立てやすいアーキテクチャが求められている
ですので、宣言的UI時代は、Composableな(部品を組み立て可能な)アーキテクチャ が求められています。
つまり、「レゴブロックのような」「UIの部品化がしやすい」「その部品を組み合わせやすい」アーキテクチャ であれば、宣言的UIのメリットを 最大限享受できる ということです。
MVVMは、Composableではない
モバイル開発で、MVVMは デファクトスタンダード 的なアーキテクチャです。
しかし、MVVMは、Composableなアーキテクチャではありません。
(MVVMが「Composable」ではない理由は、後述します)
つまり、MVVMを採用しても、UIがコンポーネント化しやすくなるわけでも、コンポーネントを組み合わせることが容易になるわけでもありません。
ですので、ぼくは、宣言的UIにはMVVMは合わないと考えて、採用を見送りました。
そこで登場するのが、「The Composable Architecture(TCA)」です。
TCAは名前の通り「Composable」なアーキテクチャ です。
「The Composable Architecture(TCA)」とは
「The Composable Architecture (TCA)」 は、一言でいうと、
「SwiftUI版のRedux」 です。
Reduxは、Flux のアーキテクチャを、副作用のない純粋関数によって実現するアーキテクチャー であり、ライブラリです。
Reduxは、同じく宣言的UIのReactで使われていて、最も普及率が高いです。
TCAを使うと、「副作用のない純粋関数」 を用いて 「人間が理解しやすい単方向の予測可能な状態変化のフロー」”でしか” コードを書けなくなります。
上記の”縛り”(レールの上)でしか、コードを書くことしかできないので、 「複雑怪奇で難解なコード」を生み出すリスクが大幅に下がり ます。
TCAは、「状態管理、Composable、テスト」 に重点を置いています。
「The Composable Architecture」を直訳すると
「複数の要素や部品などを結合して、構成や組み立てが可能なアーキテクチャ」
という意味になります。
「Composable」は、 「構成(しやすい), 組み立て(しやすい)」 という意味です。
言い換えると、
TCAは肥大化した一枚岩(モノシリック)なコードをコンポーネントとして分割することで、安全かつ迅速に分割し、見直しを可能とするアーキテクチャー
と言えます。
TCAは、Point-FreeのBrandon WilliamsとStephen Celisの二人によって開発されました。彼らは、関数型プログラミングとSwiftの開発に関する情報を提供する多くの動画を公開しています。
なぜ「The Composable Architecture(TCA)」採用するのか?
TCAは、宣言的UI時代に即したComposableなアーキテクチャです。
MVVMでは解決できない問題を解決してくれます。
以下に、TCAのメリット/デメリットをまとめます。
TCAのメリット
- 宣言的UIにFitしたComposableなアーキテクチャを導入できる
-
処理やデータの流れがシンプルになる
- 異なるコンポーネントを通過するデータの流れが明確に定義され、一方向である。これは、コードや処理の理解を容易にする。
- 大規模アプリになっても、コードが スケールする。(容易に分割できる)
- テストが書きやすい
TCAのデメリット
- 学習コストが高い
- PointFree社という、よくわからん人たちが作っていて不安です(※個人の感想です。ぼくが知らないだけかもしれません)
- 私は、PointFree社が何をしている会社なのか知りませんし、メインの二人がいなくなってしまったりすることを考えると、正直、不安に感じました。
- できて日が浅い。(Ver 1.0に達していない)
- 変更のキャッチアップが大変そう
SwiftUIにMVVMは、なぜ合わないのか
SwiftUIで、MVVMを使うデメリットを以下に挙げてみます。
-
Composableではない
- 宣言的UIでUIの部品化が進むと、コンポーネント間の接続の問題(状態管理とその状態をどうやって運ぶか)が発生する
- MVVMは、コンポーネント間の接続の問題を解決するアーキテクチャではない
- 宣言的UIでUIの部品化が進むと、コンポーネント間の接続の問題(状態管理とその状態をどうやって運ぶか)が発生する
-
異なるコンポーネントを通過するデータフローが明確に定義されず、フローの方向がぐちゃぐちゃになる。これは、コードの理解を難しくする。
-
PropertyWrapperの種類が多すぎて、ViewModelをどこに管理して、どうやって運ぶかという問題に、いちいち頭を悩ませる必要がある。
- 大規模開発において、各人のPropertyWrapperの判断基準を統一するのは難しい
- ViewModelの無秩序化によって、コードが複雑になりスケールしない。
- ViewModelとModelのやりとりが煩雑になる。
-
PropertyWrapperの種類が多すぎて、ViewModelをどこに管理して、どうやって運ぶかという問題に、いちいち頭を悩ませる必要がある。
MVVMとTCAのコードの比較
おそらく文章だけでは、MVVMのどこが駄目で、TCAのどこが良いのか伝わらないと思うので、
具体的に、MVVMとTCAのコードを比較しながら、MVVMの悪いところとTCAの良いところを解説します。
本記事で説明するコードは、以下のリポジトリに公開しています。
https://github.com/karamage/swiftui-with-mvvm-to-tca-counter
最初の例として、以下のようなシンプルなカウンターを作るとします。
・「+」を押せば、1カウントアップ
・「-」を押せば、1カウントダウン
Simple Counter(MVVM版)
これをMVVMで実装すると、以下のようなコードになります。
Model
まずは、Couterモデルを作成します。
countのInt値を保持して、incrementとdecrementメソッドを実装しています。
struct Counter {
var count = 0
mutating func increment() {
self.count += 1
}
mutating func decrement() {
self.count -= 1
}
}
ViewModel
続いて、ViewとModelの間を橋渡しするViewModelを作成します。
ObservableObjectに準拠したクラスをViewModelとして作成します。
import SwiftUI
class CounterViewModel:ObservableObject {
@Published var counter = Counter()
var count: Int {
return counter.count
}
func increment() {
counter.increment()
}
func decrement() {
counter.decrement()
}
}
View
Viewでは、ViewModelを@StateObject
で状態管理&購読します。
import SwiftUI
struct CounterView_MVVM: View {
@StateObject private var counterViewModel = CounterViewModel()
var label: String
var body: some View {
HStack {
Text("\(label):")
.padding()
.font(.subheadline)
Button("-") { counterViewModel.decrement() }
Text("\(counterViewModel.count)").font(.body.monospacedDigit())
Button("+") { counterViewModel.increment() }
}
}
}
struct CounterView_MVVM_Previews: PreviewProvider {
static var previews: some View {
CounterView_MVVM(label: "Counter")
}
}
デモ画面
import SwiftUI
struct SimpleCounterPage_MVVM: View {
private let readMe = "Single Counter with MVVM"
var body: some View {
Form {
Section(header: Text(readMe)) {
CounterView_MVVM(label: "Counter")
.buttonStyle(.borderless)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
.navigationBarTitle("SimpleCounter")
}
}
struct SimpleCounterPage_MVVM_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
SimpleCounterPage_MVVM()
}
}
}
いい感じに実装できました。
(そもそも「ViewModelの存在って意味ないやん」ってツッコミもあるかと思います。そのことについては、前回の記事に詳しく書きましたので、合わせて御覧ください)
Simple Counter(TCA版)
続いて、同じことをTCAで実装してみます。
Store
まずは、Storeを実装します。
CounterStateでInt値を保持して、increment/decrementのアクションを定義しています。
import ComposableArchitecture
struct CounterState: Equatable {
var count = 0
}
enum CounterAction: Equatable {
case decrementButtonTapped
case incrementButtonTapped
}
struct CounterEnvironment {}
let counterReducer = Reducer<CounterState, CounterAction, CounterEnvironment> { state, action, _ in
switch action {
case .decrementButtonTapped:
state.count -= 1
return .none
case .incrementButtonTapped:
state.count += 1
return .none
}
}
View
Viewは、Storeの状態を参照したり、Actionを呼び出したりします。
import ComposableArchitecture
import SwiftUI
struct CounterView: View {
let store: Store<CounterState, CounterAction>
var label: String
var body: some View {
WithViewStore(self.store) { viewStore in
HStack {
Text("\(label):")
.padding()
.font(.subheadline)
Button("-") { viewStore.send(.decrementButtonTapped)}
Text("\(viewStore.count)").font(.body.monospacedDigit())
Button("+") { viewStore.send(.incrementButtonTapped) }
}
}
}
}
デモ画面
import ComposableArchitecture
import SwiftUI
struct SimpleCounterPage: View {
private let readMe = "Single Counter with TCA"
var body: some View {
Form {
Section(header: Text(readMe)) {
CounterView(
store: Store(
initialState: CounterState(),
reducer: counterReducer,
environment: CounterEnvironment()
),
label: "Counter"
)
.buttonStyle(.borderless)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
.navigationBarTitle("SimpleCounter")
}
}
struct SimpleCounterPage_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
SimpleCounterPage()
}
}
}
TCAもいい感じにできました。
Simple CounterのMVVMとTCAを見比べる
正直、MVVMでもTCAでも 「どちらでも大差ないな」 と思った方、たしかにそうです。
MVVMとTCAの違いは、「Model + ViewModel」だった部分が、「Store」に変わっただけのように見えます。
このように、シンプルなアプリでは、TCAを採用しても大した違いはありません。
だがしかし、ここからが本題です。
恐怖の仕様追加
「Simple Counter」を完成して安心していた私に、客先から仕様追加が言い渡されました。
「Counterを、もう一個追加してほしいんだよねー」
「もう一個追加するくらい簡単でしょ?」
「ただ同じものを追加するんじゃ面白くないから、もう一つのカウンターはランダムな値をカウントアップしてよ」
TwoCounter仕様
- 2つのカウンター「Counter」「Random Counter」を表示する。
- 「Counter」は、1ずつインクリメント/デクリメントする
- 「Random Counter」は、押されるたびに1...10のランダムな値をインクリメント/デクリメントする
ぼくは、ちょっと悩みましたが、「簡単ですよ!」と答えて、実装に取り掛かりました。
これを読んでいるあなたも、どのように変更するべきか考えてみてください。
(※以下のMVVMの変更は、問題のある例として載せています)
TwoCounter(MVVM版)
Model
まずは「ランダムにカウントする」というロジックをModelに実装しました。
struct Counter {
var count = 0
mutating func increment() {
self.count += 1
}
mutating func decrement() {
self.count -= 1
}
+ mutating func incrementRandom10() {
+ self.count += random10()
+ }
+
+ mutating func decrementRandom10() {
+ self.count -= random10()
+ }
+
+ private func random10() -> Int {
+ Int.random(in: 1 ... 10)
+ }
}
ViewModel
橋渡しするViewModelにも、「ランダムにカウントする」メソッドを追加しました。
import SwiftUI
class CounterViewModel:ObservableObject {
@Published var counter = Counter()
var count: Int {
return counter.count
}
func increment() {
counter.increment()
}
func decrement() {
counter.decrement()
}
+ func incrementRandom10() {
+ counter.incrementRandom10()
+ }
+
+ func decrementRandom10() {
+ counter.decrementRandom10()
+ }
}
View
続いて、RandomCounterのViewを新規に作りました。
このRandomCounterViewでは、「incrementRandom10」「decrementRandom10」の呼び出しを行うようにしています。
+ import SwiftUI
+
+ struct RandomCounterView_MVVM: View {
+ @StateObject private var counterViewModel = CounterViewModel()
+ var label: String
+
+ var body: some View {
+ HStack {
+ Text("\(label):")
+ .padding()
+ .font(.subheadline)
+ Button("-") { counterViewModel.decrementRandom10() }
+ Text("\(counterViewModel.count)").font(.body.monospacedDigit())
+ Button("+") { counterViewModel.incrementRandom10() }
+ }
+ }
+ }
+
+ struct RandomCounterView_MVVM_Previews: PreviewProvider {
+ static var previews: some View {
+ RandomCounterView_MVVM(label: "Random Counter")
+ }
+ }
デモPage
最後に、2つのカウンターをページに配置して、完成です。
import SwiftUI
struct TwoCounterPage_MVVM: View {
private let readMe = "Two Counter with MVVM"
var body: some View {
Form {
Section(header: Text(readMe)) {
VStack {
CounterView_MVVM(label: "Counter")
.buttonStyle(.borderless)
.frame(maxWidth: .infinity, maxHeight: .infinity)
+ RandomCounterView_MVVM(label: "Random Counter")
+ .buttonStyle(.borderless)
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
}
.navigationBarTitle("TwoCounter")
}
}
struct TwoCounterPage_MVVM_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
TwoCounterPage_MVVM()
}
}
}
やったー、ばっちり、うまく動作しています!
MVVMの問題点
以上の変更は、一見すると良い変更に思えますが、いくつかの問題をコードに紛れ込ませています。
CounterViewのコンポーネント化を破壊している
最初の問題は、「CounterView」は、再利用可能なコンポーネント だったはずなのに、新たに 「RandomCounterView」という亜種 を生み出してしまったことです。
これで、「CounterView」は再利用できないコンポーネントに成り下がったので、以降の仕様変更では、「CounterView」の亜種を生み出し続けることになります。
また、CounterViewModelに
- 「1ずつカウントアップする」
- 「ランダムにカウントアップする」
という Viewに依存したロジックが、混在 しています。
- 「CouterViewでは、1ずつカウントアップする」
- 「RandomCouterViewでは、ランダムにカウントアップする」
という処理のフローが、暗黙知として隠され、ViewModelを一見しただけわからない状態になっています。
本来であれば、ViewとViewModelの関係は、1:1 が望ましいのですが、2:1 の関係になっています。
関係性が、3,4...と増えていくと、複雑で可読性の低いコードが簡単に書けてしまいます。
以上が、ぼくが 「MVVMがComposableではない」 と言っている理由です。
宣言的UIを使ってUIのコンポーネント化がやりやすくなったはずなのに、なぜかコンポーネントを組み合わせて画面を構成することがやりにくくなってしまっている のです。
これは、かなりもったいない状態だと思います。
追記(2022/04/25)
この記事の補足解説として、MVVMのままでもComposableな実装を保つやり方が、りずさんのブログに説明されています。
りずさんのおっしゃっているTCAへの懸念も、もっともだと思います。
合わせてご覧ください。
SwiftUIでのMVVM例 - 本当にMVVMはComposableではないのか
https://tech.caph.jp/swiftui-mvvm-is-composable/
その他のご意見
TwoCounter(TCA版)
一方、TCAでは、CounterViewを再利用するかたちで、状態管理とロジックを切り分けて、最小限の変更で実装できます。
まずは、CounterStoreに、「ランダムにカウントアップする」というロジックを切り出すために「randomCounterReducer」を作成します。
import ComposableArchitecture
struct CounterState: Equatable {
var count = 0
}
enum CounterAction: Equatable {
case decrementButtonTapped
case incrementButtonTapped
}
struct CounterEnvironment {}
let counterReducer = Reducer<CounterState, CounterAction, CounterEnvironment> { state, action, _ in
switch action {
case .decrementButtonTapped:
state.count -= 1
return .none
case .incrementButtonTapped:
state.count += 1
return .none
}
}
+ let randomCounterReducer = Reducer<CounterState, CounterAction, CounterEnvironment> { state, action, _ in
+ switch action {
+ case .decrementButtonTapped:
+ state.count -= random10()
+ return .none
+ case .incrementButtonTapped:
+ state.count += random10()
+ return .none
+ }
+ }
+
+ private func random10() -> Int {
+ Int.random(in: 1 ... 10)
+ }
続いて、2つのカウンターのStoreを分離して管理するために、新たにCountersStoreを作成します。
import ComposableArchitecture
import SwiftUI
struct CountersState: Equatable {
var counter1 = CounterState()
var counter2 = CounterState()
}
enum CountersAction {
case counter1(CounterAction)
case counter2(CounterAction)
}
struct CountersEnvironment {}
let countersReducer = Reducer<CountersState, CountersAction, CountersEnvironment>
.combine(
counterReducer.pullback(
state: \CountersState.counter1,
action: /CountersAction.counter1,
environment: { _ in CounterEnvironment() }
),
randomCounterReducer.pullback(
state: \CountersState.counter2,
action: /CountersAction.counter2,
environment: { _ in CounterEnvironment() }
)
)
View
TCAでは、Viewの変更も新規に作成も行いません。
既存のCounterViewをそのまま再利用します。
デモページ
最後に、2つのCounterViewを並べて、完成です。
import ComposableArchitecture
import SwiftUI
struct TwoCounterPage: View {
private let readMe = "Two Counter with TCA"
let store: Store<CountersState, CountersAction>
var body: some View {
Form {
Section(header: Text(readMe)) {
VStack {
CounterView(
store: self.store.scope(state: \.counter1, action: CountersAction.counter1)
,
label: "Counter"
)
.buttonStyle(.borderless)
.frame(maxWidth: .infinity, maxHeight: .infinity)
+ CounterView(
+ store: self.store.scope(state: \.counter2, action: CountersAction.counter2),
+ label: "Random Counter"
+ )
+ .buttonStyle(.borderless)
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
}
.navigationBarTitle("TwoCounter")
}
}
struct TwoCounterPage_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
TwoCounterPage(
store: Store(
initialState: CountersState(),
reducer: countersReducer,
environment: CountersEnvironment()
)
)
}
}
}
どうでしょう?
TCAであれば、簡単にCounterViewを再利用できる ことがわかると思います。
まとめ
TCAは、 「複数の要素や部品などを結合して、構成や組み立てが可能な、Composableなアーキテクチャ」 です。
SwiftUIなどの宣言的UIでは、UIのコンポーネント化が容易になります。
よって、コンポーネント化が進めば進むほど、SwiftUIとTCAの相性は抜群によいです。
ですので、ぼくは、SwiftUIには、TCAを採用することをおすすめします。
しかしながら、TCAの学習コストの高さや、開発体制に不安 があるのもわかりますので、様子見する気持ちもまたわかります。(ぼくもTCAを導入する際に、長い間、二の足を踏んでいました)
SNSでも、この点にリスクを感じている人がいました。
その気持ちわかります!
TCAは、関数型プログラミングに触れてこなかった人には理解するのが難しいですし、モチベーション的にもキツイと思います。
ですが、まずはじめに、TCAをちょっとさわってみるだけでもよい のではないかなと思います。
食わず嫌いになっているだけかもしれませんし、ちょっとやって駄目だったらやめればよいだけの話です。
また、繰り返しになりますが、アーキテクチャを採用する際、そのアプリに合ったアーキテクチャを ”よく考えて” 採用することをオススメします。
この記事は、「TCAが絶対の正解だ」と言いたいわけではありません。
アメリカのことわざに 「ハンマーしか持っていなければ、すべてが釘のように見える」 というものがあります。
ひとつの手段に囚われると、 その手段が目的化してしまう ことへの戒めとして語られることが多い言葉です。
MVVMだけにとらわれると、MVVMすることが目的化しやすいのです。
同じことは、TCAにも言えます。
よって、複数のアーキテクチャパターンを理解し、選択肢を持った状態 で、アプリに合ったものを選ぶことが大事です。
仮に考えた結果が 「やっぱりMVVMを採用しよう」 であったとしても、それはそれで良いと考えて、その考えを否定するつもりは一切ありません。
ご理解いただけるとうれしいです。
最後までお読みいただき、ありがとうございました。
おまけ(※2022/04/24追記)
TCAの便利さを、もう少し説明したいため、追記します。
さらなる仕様変更
先程作ったTwoCounterに、またまた客先から仕様変更が飛んできました。
「Random Counterを押したときに、Counterもカウントアップしてほしいんだよねー」
「Counterを押したときは、今のままでよいからさ」
「ちょっと動作変えるだけだし簡単でしょ?」
TwoCounter仕様(Ver.2.0)
- 2つのカウンター「Counter」「Random Counter」を表示する。
- 「Counter」は、1ずつインクリメント/デクリメントする
- 「Random Counter」は、押されるたびに1...10のランダムな値をインクリメント/デクリメントする、かつ「Counter」を、1ずつインクリメント/デクリメントする
TwoCounter Ver.2.0 (TCA版)
この変更をMVVMでやろうとすると、かなり大変で コードが複雑になるのは必至 です。
しかし、TCAで変更するなら、ちょー簡単です。
まずは、以下のExtentionをコピペして貼り付けます。
下記のresendingメソッドは、「あるReducerのAction実行後に、別のReducerのActionを実行したいとき」 に便利ですので、メモしておきましょう。
extension Reducer {
func resending<Value>(
_ extract: @escaping (Action) -> Value?,
to embed: @escaping (Value) -> Action
) -> Self {
.combine(
self,
.init { _, action, _ in
if let value = extract(action) {
return Effect(value: embed(value))
} else {
return .none
}
}
)
}
func resending<Value>(
_ `case`: CasePath<Action, Value>,
to other: CasePath<Action, Value>
) -> Self {
resending(`case`.extract(from:), to: other.embed(_:))
}
func resending<Value>(
_ `case`: CasePath<Action, Value>,
to other: @escaping (Value) -> Action
) -> Self {
resending(`case`.extract(from:), to: other)
}
func resending<Value>(
_ extract: @escaping (Action) -> Value?,
to other: CasePath<Action, Value>
) -> Self {
resending(extract, to: other.embed(_:))
}
}
あとは、reducerに以下の 一行を加えれば完成 です!
TCAって便利でスゴイですね!
let countersReducer = Reducer<CountersState, CountersAction, CountersEnvironment>
.combine(
counterReducer.pullback(
state: \CountersState.counter1,
action: /CountersAction.counter1,
environment: { _ in CounterEnvironment() }
),
randomCounterReducer.pullback(
state: \CountersState.counter2,
action: /CountersAction.counter2,
environment: { _ in CounterEnvironment() }
)
)
// RandomCounterのReducerのAction実行後に、CounterのActionを実行したい場合
+ .resending(/CountersAction.counter2, to: /CountersAction.counter1)
コメント
@takuro70212
@karamage0
@takuro70211
@hiro_t
2
@karamage0
@akemi-mizuki(編集済み)
1
@karamage0
@akemi-mizuki0
qiitaのトップページを見たらこの記事があって、デザインパターンについては最近考えることがあったのでswiftUIは使ったことがないどころかswiftも使ったこともないですが読んでみました。
気になったことがあるのですが、MVVMがComposableではない理由です。TwoCounterは僕だったら、Model、Viewmodelを二つに分けます。後はviewは記事でも分かれてるのでそれぞれ別のViewModelを使うようにすれば記事で書いている問題点はないように思えます。もしかしたらMVCを普段使っているのでMVVMじゃなくなっていたらすいません。
@takuro7021 さん
読んでいただき、ありがとうございます😊
Swift知らない人にも読んでもらえて嬉しいです。
はい、分けてもよいです!的確なご意見だと思います。
ただ、Model、Viewmodelを分けただけではRandomCouterViewを作る必要があるので、「Composableではない」という問題は残ります。
部品化されたコンポーネントを組み合わせるだけで画面を作りたいのに、それができないのです。
TwoCounterの例では、MVVMがコンポーネントの再利用を妨げている例として提示しています。
(CouterViewコンポーネントをそのまま再利用しつつ、ロジックだけを切り替えたい)
TCA版では、簡単にCouterViewコンポーネントをそのまま再利用することができています。
どうやらコンポーネントとComposableというのを勘違いしていたようです。自分の認識では、Model、ViewModel、Viewをセットでコンポーネントだと思ってました。そしてComposableというのは例えるならブログパーツみたいなものとして使えるというイメージでした。でも確かにTCA版だとViewを再利用しています。
となるとMVVMだと確かに難しいと思います。ジェネリクスを使うかViewmodelを引数にして使えるようにすればできなくもないですけどそこまでするならTCA使った方が楽そうです。ただ改めて読み直すとあくまでTCAだと簡単にできるように機能を提供されているだけのようにも思えますが。
Swiftは分からないので、それについてはなんとも言えませんが、
については、
WPFでの、MVVMは、Viewの再利用は、Usercontrolなどを利用すれば、OKです。
自分も勉強になるとコードを書いてみました。
ユーザーコントロールの
CounterCommonView
を、MainView
に2つ配置し、それぞれ、
・
CounterViewModel
・
RandomCounterViewModel
にBindingします。
また、機能の差が小さいなら、ViewModelを2つ利用しなくても、
例えば、CounterCommonViewModel にして、1つのViewModelにしても構わないと、考えます。
また、Modelも、
・
CounterMain
・
RandomCounterMain
の2つにしていますが、同様に、機能の差が小さいなら、1つのModelにまとめても構わないと考えます。
TwoCounter仕様(Ver.2.0) について
また私がこの機能を実装するなら、ViewModel上で処理しないでしょう。理由は、
・Modelで行うべき処理だと考えます。
・
CounterViewModel
とRandomCounterViewModel
とが、どちらかに依存することは無い。無関係がセオリーのため。従って、
CounterMain
RandomCounterMain
で機能を実装します。MVVMは、人によって考え方が異なり、従って実装の方法も異なり、何が正解かは難しいのですが、現時点の私の理解によれば、WPFでは、以上のように構築しますね。
MainView
MainViewModel
CounterCommonView

ファイル構成

@hiro_t さん
詳細な補足説明ありがとうございます。
WPFについて、恥ずかしながら無知なのですが、
ViewModelがキレイに分割され、コンポーネントの再利用もできて、いいですね。
ありがとうございます、WPF MVVMの理解が深まりました。
PointFreeの技術力は公開しているライブラリやTCAのソースコードを読めば技術力がないとは言えないと思いますし、ブログなどでも積極的に多くの情報公開していますし、issueやdiscussions、forumでも頻繁に関係者がコメントしてますし、多くの人に利用されているライブラリで下手のOSSよりも信用が置けると思うのですが、具体的にどんな理由で信用が置けないのでしょうか?
本当に自身が知らない会社ってだけで、信用ができないライブラリという烙印を押したってことではないですよね…?
個人的にはこの記事を読む限り、karamageさんがTCAを本格的に利用したことがないように見受けられたので、TCAを少し触ったことあるだけの人が有用なライブラリを根拠なくネガティブに評価しているのはちょっとどうかなと思いコメントしました(間違っていたらすみません)。
@akemi-mizuki さん。
ご指摘ありがとうございます。
はい、メインのお二人の技術力は、コードや動画を見て、その点は信用しています。
ですので、記事の方も表現を修正しました。
私は、PointFree社が何をしている会社なのか知りません。
(大量の動画があるPointFreeのサイトを運営していることは知っています。これがメイン事業(?)なのでしょうか)
どこを見れば、PointFree社が信用できるとわかりますか。
教えていただけるとありがたいです。
もしメインの二人がいなくなってしまったり、この会社がなくなったりした場合のことを考えると、正直、不安に感じました。
「多くの人に利用されている」というのは、知りませんでした。
私は、大手アプリでのTCA採用実績を聞いたことがありません。
もし、採用しているアプリがあるのであれば、具体的に教えていただけませんか。
安心材料になると思います。
少しはご自身で調べたらいかがでしょうか…
個人で開発されている有名で有用なOSSなどいくらでもあるかと思いますが、そういったOSSも信用されていないんですね。
信用には絶対的な指標はないと思っていて、OSSの場合は極論ですがアウトプットから判断するしかないのかなと思いますが、karamageさんの場合、どんな企業が作っているかが最重要なんですね。
その一方で resending のコードは他のサイトからのコピペで、他人の書いたコードを信用されていて、自分が書いたコードのように見せて他人に薦めるのはすごいメンタルですね。