Compoosable Forms로 TCA 보일러 패널 제거

24039 단어 iOSSwiftSwiftUITCAtech
이번에는 TCA의 Compoosable Forms에 대해 설명하고 싶습니다.
비슷한 내용으로 iOS 앱 개발을 위한 Functional Archeitecture 정보 공유회에서 Compoosable Forms가 어떻게 고려해서 완성되었는지 발표했습니다. 가능하면 참고하세요.🙏
요컨대 기사 속 Compoosable Forms가 어떻게 사용되는지에 초점을 맞춰 설명하고자 합니다.

Compoosable Forms란 무엇입니까?


TCA에서 State는 작업을 통해서만 변경되며 모든 변경은 Reducter에서 수행됩니다.부작용 관련 처리가 있으면 Effect도 사용합니다.
이러한 원칙에 따라 코드를 쓰면 TCA의 장점은 처리 절차가 명확한 코드를 쓸 수 있지만 약점도 있다.
약점이 있다면 모든 상태의 변경은 동작을 통해 이루어지기 때문에 단순한 상태의 변경이라도 동작을 통해 이루어져야 한다는 점이 약해진다.
구체적으로 다음과 같은 예가 있다.
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에서 상태 관리를 하는 이상 동작을 통해서만 스테이트를 변경할 수 있기 때문에 단순한 상태 변경이라도 매번 이런 코드를 써야 하는 궁지에 몰릴 수 있다.
Compoosable Forms를 사용하면 TCA에 코드를 쓸 때 이런 보일러 보드 코드와 작별을 고할 수 있습니다.
Compoosable Forms를 어떻게 사용하는지 좀 설명하고 싶습니다.

Compoosable Forms 사용 방법


방금 보일러판 코드로 보여준 BoilerPlate.swift 을 Compoosable Forms로 다시 써서 사용법을 소개하고 싶습니다.
그나저나 지금까지 Compoosable Forms를 소개했지만 TCA의 v0입니다.14.0의 릴리즈로 인해 Form에서 Binding으로 변경되었으므로 Compoosable Bindings가 더 정확할 수 있음🙏
헷갈리기 쉬우니까 여기서부터 Compoosable Bindings라는 명칭을 통일하고 싶어요.

State


먼저 State에 대해 살펴보겠습니다.
State는 일반적으로 정의됩니다.
그래서 아까는 특별한 변경 없이 다음과 같은 형식으로 바뀌었다.
struct ExampleState: Equatable {
  var age = 0
  var displayName = ""
  var protectMyPosts = false
  var sendNotifications = false
}

Action


다음은 동작입니다.
Action은 다음 코드만 있으면 됩니다.
enum ExampleAction: Equatable {
-  case ageChanged(Int)
-  case displayNameChanged(String)
-  case protectMyPostsChanged(Bool)
-  case sendNotificationsChanged(Bool)
+  case binding(BindingAction<ExampleState>)
}
BindingAction을 처리하는 State를 지정하면 됩니다.

Reducer


마지막으로 Reducter.
Reducter도 상당히 짧기 때문에 코드를 먼저 표시합니다.
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)
Compoosable Bindings의 기법만 사용하면 보일러판의 전선을 제거할 수 있다🎉
Reducter가 특별한 일을 했으니까 설명해 주세요.
우선 아래 부분입니다.
.binding(action: /ExampleAction.binding)
BindingAction을 처리할 때 TCA의higher-order reducter를 사용하여 지정해야 합니다.
자세한 내용은 Point-Free의 글이나 처음에 소개한 자신의 발표 슬라이드에도 쓰여 있습니다. 구조 등에 신경을 쓰신다면 위의 내용을 참조하시기 바랍니다.🙏
구조에 신경 안 쓰면 Binding Action을 처리할 때 이런 느낌으로 CasePath를 지정하면 돼요. 그렇게 생각하면 괜찮아요.🙆‍♂️
다음은 다음 부분입니다.
case .binding:
  return .none
enum인 이상 처리를 해야 하기 때문에 case .bindingnoneEffect로 되돌아오는 처리만 합니다.
Compoosable Bindings를 사용하면 간단한 State 변경은 여기에 소개된 .binding(action: /ExampleAction.binding)case .binding에 다시 쓰기none로 처리하면 View에서 Action을 통해 State를 변경할 수 있습니다.
구체적으로 뷰의 처리 방법을 설명하기 전에 Reducter의 마지막 부분을 설명합니다.
마지막으로 설명한 부분은 다음과 같다.
case .binding(\.sendNotifications):
  guard state.sendNotifications
  else {
    state.sendNotifications = false
    return .none
  }
단순한 State 변경이라면 이런 처리를 쓸 필요가 없다. 예를 들어 단순히 State를 변경한 후에 그 State를 바탕으로 어떤 처리(예를 들어 발리일 등)를 하려면 이런 코드를 써야 한다.
구체적으로 위 코드와 같이 sendNotifications의 State가 변경된 후 처리하려면 먼저 sendNotifications를 가리키는 KeyPath를 지정합니다.
이후 그 스테이트에 대한 검증 논리 등을 쓰는 절차가 됐다.

View


마지막으로 뷰에서 Action을 보내는 방법에 대해 간단히 설명하고 싶습니다.
하나의 예만 있으면 충분합니다. displayName State를 변경하고 싶을 때의 코드 예는 다음과 같습니다.
  TextField(
   "DisplayName",
   text: viewStore.binding(
-    get: { $0.displayName },
-    send: { ExampleAction.displayNameChanged }
+    keyPath: \.displayName,
+    send: ExampleAction.binding
   )
 )
Compoosable Bindings를 사용하지 않으면 get 원하는 Statesend에 액션을 지정합니다.
Compoosable Bindings를 사용하면 keyPathsend 두 매개변수가 입력됩니다.keyPath에 해당하는 State의 KeyPath를 지정하고send 에 Action을 지정합니다.
간단한 State 변경 가능🙌

보태다


여기까지 읽으면 편한 사람도 있고 그렇지 않은 사람도 있다.
다만, Compoosable Bindings의 강점은 고정된 정형문만 쓰면 이후 State를 추가하면 State의 동작, Reducter로 완성할 수 있는 상태를 바꿀 수 있다는 것이라고 생각한다.
구체적으로 다음과 같은 State가 State에 새로 추가되었습니다.
struct ExampleState: Equatable {
  var age = 0
  var displayName = ""
  var protectMyPosts = false
  var sendNotifications = false
+ var sendEmailNotifications = false
+ var sendMobileNotifications = false
}
뷰에서 지정 가능binding하면 해당 State를 변경할 수 있음👏

끝말


TCA에 코드를 쓸 때 보일러 보드에 대해 고민하기 시작하면 Compoosable Bindings를 사용하면 쉽게 코딩할 수 있을 것 같아요🙏
이번 기사에서 Compoosable 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

좋은 웹페이지 즐겨찾기