Learn how to connect TCA features to SwiftUI bindings for two-way communication
Many SwiftUI APIs use bindings to set up two-way communication between your application’s state and a view. The Composable Architecture provides several tools for creating bindings that establish such communication with your store.
For screens with many controls, creating individual actions for each binding can be tedious. The library provides BindableAction and BindingReducer to eliminate boilerplate.
Consider a settings screen with many editable fields:
@Reducerstruct Settings { @ObservableState struct State { var digest = Digest.daily var displayName = "" var enableNotifications = false var isLoading = false var protectMyPosts = false var sendEmailNotifications = false var sendMobileNotifications = false } // ...}
Traditionally, you’d need an action for each field:
enum Action { case digestChanged(Digest) case displayNameChanged(String) case enableNotificationsChanged(Bool) case protectMyPostsChanged(Bool) case sendEmailNotificationsChanged(Bool) case sendMobileNotificationsChanged(Bool)}
And handle each in the reducer:
var body: some Reducer<State, Action> { Reduce { state, action in switch action { case let digestChanged(digest): state.digest = digest return .none // ... 5 more cases! } }}
Binding actions can be tested just like regular actions. Instead of sending a specific action like .displayNameChanged("Blob"), you send a BindingAction:
let store = TestStore(initialState: Settings.State()) { Settings()}await store.send(\.binding.displayName, "Blob") { $0.displayName = "Blob"}await store.send(\.binding.protectMyPosts, true) { $0.protectMyPosts = true}
The first argument is a key path to the binding, and the second is the new value.