😎

TCA のボイラープレートを Composable Forms で解消する

2021/03/21に公開

今回は TCA の Composable Forms というものについて説明しようと思います。

似た内容として、iOSアプリ開発のためのFunctional Architecture情報共有会では、Composable Forms がどのような考えで出来上がったのかということについて発表させて頂きましたので、よろしければ参考にしてください🙏

あくまでも記事では Composable Forms はどうやって使うことができるのかという部分に焦点を当てて説明をしていこうと思います。

Composable Forms とは?

TCA では、 State は Action を通じてのみ変更され、全ての変更は Reducer で行われます。もし、副作用が絡む処理がある場合は Effect も利用します。
このような原則に従ってコードを書いていれば、処理の流れが非常に明確なコードを書けることが TCA の強みではありますが、弱みでもあります。

何が弱みなのかというと、全ての状態の変更は Action を通じて行われるため、単純な状態の変更であっても必ず Action を経由しなければならないという点が弱みになります。

具体的には以下のような例が考えられます。

BoilerPlate.swift
struct ExampleState: Equatable {
  var age = 0
  var displayName = ""
  var protectMyPosts = false
  var sendNotifications = false
}

enum ExampleAction: Equatable {
  case ageChanged(Int)
  case displayNameChanged(String)
  case protectMyPostsChanged(Bool)
  case sendNotificationsChanged(Bool)
}

let exampleReducer =
  Reducer<ExampleState, ExampleAction, Void> { state, action, _ in
    switch action {
    case let .ageChanged(age):
      state.age = age
      return .none
      
    case let .displayNameChanged(name):
      state.displayName = name
      return .none
      
    case let .protectMyPostsChanged(protectMyPosts):
      state.protectMyPosts = protectMyPosts
      return .none
      
    case let .notificationsChanged(sendNotifications):
      guard sendNotifications
      else {
        state.sendNotifications = sendNotifications
	return .none
      }
    }
  }

上記の exampleReducer で行われている処理は、単純な値を代入しているか、軽く条件分岐を行ってから代入をしているかというものしかありません。
仮に TCA で状態を管理しないとしたならば、単純な代入式一つ書けば済むようなものになっています。
しかし、TCA で状態管理を行う以上は Action を通じてのみ、State を変更する必要があるため、単純な状態の変更であっても毎回このようなコードを書く羽目になります。

Composable Forms を使えば、TCA でコードを書く時にこのようなボイラープレートコードとおさらばすることができるようになります。

Composable Forms をどうやって使うかを軽く説明していこうと思います。

Composable Forms の使い方

先ほどボイラープレートコードの例として示した BoilerPlate.swift を Composable Forms で書き換えることによって、使い方を紹介しようと思います。
ちなみに今まで、Composable Forms と紹介していましたが、TCA の v0.14.0 のリリースで Form から Binding に名称が変更になったため、Composable Bindings と言った方が正しいかもしれません🙏
紛らわしいので、ここからは Composable Bindings という名称で統一しようと思います。

State

まず、State についてです。
State は通常通り定義します。
そのため、先ほどど特に変更はなく、以下のような形になります。

struct ExampleState: Equatable {
  var age = 0
  var displayName = ""
  var protectMyPosts = false
  var sendNotifications = false
}

Action

次に Action です。
Action はなんと、以下のコードだけで良くなります。

enum ExampleAction: Equatable {
-  case ageChanged(Int)
-  case displayNameChanged(String)
-  case protectMyPostsChanged(Bool)
-  case sendNotificationsChanged(Bool)
+  case binding(BindingAction<ExampleState>)
}

BindingAction に扱う State を指定してあげれば OK です。

Reducer

最後に Reducer です。

Reducer もかなり短いので先にコードを示します。

let exampleReducer = 
  Reducer<ExampleState, ExampleAction, Void> { state, action, _ in
    switch action {
-    case let .ageChanged(age):
-      state.age = age
-      return .none
      
-    case let .displayNameChanged(name):
-      state.displayName = name
-      return .none
      
-    case let .protectMyPostsChanged(protectMyPosts):
-      state.protectMyPosts = protectMyPosts
-      return .none
      
-    case let .notificationsChanged(sendNotifications):
-      guard sendNotifications
-      else {
-        state.sendNotifications = sendNotifications
-	 return .none
-      }

+    case .binding:
+      return .none

+    case .binding(\.sendNotifications):
+      guard state.sendNotifications
+      else {
+        state.sendNotifications = false
+        return .none
+      }
+    }
  }
+ .binding(action: /ExampleAction.binding)

Composable Bindings の記法を使うだけで、ボイラープレートコードを消し去ることができているのがわかるかと思います🎉

Reducer では少し特殊なことをしているので、説明します。

まず以下の部分についてです。

.binding(action: /ExampleAction.binding)

BindingAction を扱う場合は、TCA の higher-order reducer というものを使って、このように指定してあげる必要があります。
詳しくは Point-Free の記事か、冒頭で紹介した自分の発表スライドにも書いているので、仕組みなど気になるか方はそちらをご参照ください🙏
仕組みが気にならなければ、BindingAction を扱うときはこんな感じで CasePath を指定してあげれば良いんだなくらいに思っておけば問題ないと思います🙆‍♂️

次に、以下の部分についてです。

case .binding:
  return .none

enum である以上、何らか処理はしてあげなければならないので、case .binding の時には none Effect を返却するだけという処理を行っています。

Composable Bindings を使えば、単純な State の変更についてはここまでで紹介した .binding(action: /ExampleAction.binding)case .bindingnone を返却する処理さえ書いてしまえば、後は View から Action を通じて State を変更することが可能になります。

具体的に View からはどのように扱うことができるのかを説明する前に、Reducer の最後の部分について説明します。

最後に説明する部分は以下の部分になります。

case .binding(\.sendNotifications):
  guard state.sendNotifications
  else {
    state.sendNotifications = false
    return .none
  }

単純な State の変更であればこのような処理を書く必要はないのですが、例えば単純に State を変更した後で、その State に基づいて何か処理を行いたい(例えばバリデーションなど)場合は、このようなコードを書く必要が出てきます。

具体的には上記に示したコードのように、sendNotifications という State が変更された後で処理を行いたい場合は、まず sendNotifications を指す KeyPath を指定します。
その後で、その State に関わるバリデーションロジックなどを書いていくという流れになります。

View

最後に View からはどのように Action を送ることができるかということについて軽く説明しようと思います。

一つだけ例があれば十分だと思うので、displayName State を変更したい場合のコード例を以下に示します。

  TextField(
   "DisplayName",
   text: viewStore.binding(
-    get: { $0.displayName },
-    send: { ExampleAction.displayNameChanged }
+    keyPath: \.displayName,
+    send: ExampleAction.binding
   )
 )

Composable Bindings を使わない場合だと、get に取得したい State, send に Action を指定していました。

Composable Bindings を使う場合は、keyPathsend という二つの引数を埋めることになります。keyPath には該当 State の KeyPath を指定し、send には binding Action を指定してあげます。

これだけで単純な State の変更を行うことができるようになります🙌

補足

ここまで読んで頂いて、便利だと思った方もいれば思わない方もいるかもしれません。

ただ、Composable Bindings の強みは一旦決まりきった定型文だけ書いてしまえば、後は State を追加するだけで State を変更するための Action, Reducer は出来上がった状態になっているという部分になるかなと思っています。

具体的には Stateに新たに以下のような State を追加したとします。

struct ExampleState: Equatable {
  var age = 0
  var displayName = ""
  var protectMyPosts = false
  var sendNotifications = false
+ var sendEmailNotifications = false
+ var sendMobileNotifications = false
}

これだけで View からは viewStore.binding(keyPath: \.sendEmailNotifications, send: ExampleAction.binding) と指定すれば該当の State が変更できるようになっています👏

おわりに

TCA でコードを書いている時、ボイラープレートに悩み始めたら Composable Bindings を使えば快適にコーディングすることができるようになりそうですね🙏
今回の記事では、Composable Bindings がどのような仕組みで動いているかという部分についてはほぼ省略させて頂いたため、気になる方がいらっしゃれば Point-Free さんの記事か冒頭で紹介した発表資料を参照していただければと思います🙏

TCA にはどんどん機能が追加されていっているので、今後もキャッチアップしつつ記事にできたらと思っています。

参考

https://www.pointfree.co/blog/posts/52-composable-forms-say-bye-to-boilerplate

https://www.pointfree.co/collections/case-studies/concise-forms

https://github.com/pointfreeco/swift-composable-architecture/releases/tag/0.14.0

Discussion