Documentation Index Fetch the complete documentation index at: https://mintlify.com/pointfreeco/swift-composable-architecture/llms.txt
Use this file to discover all available pages before exploring further.
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.
Ad hoc bindings
The simplest tool is to create a dedicated action that changes a piece of state.
Define state property
@Reducer
struct Settings {
struct State : Equatable {
var isHapticsEnabled = true
// ...
}
// ...
}
Define corresponding action
@Reducer
struct Settings {
struct State : Equatable { /* ... */ }
enum Action {
case isHapticsEnabledChanged ( Bool )
// ...
}
// ...
}
Handle the action
var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case let . isHapticsEnabledChanged (isEnabled) :
state. isHapticsEnabled = isEnabled
return . none
// ...
}
}
}
Derive binding in view
First, hold the store in a bindable way: struct SettingsView : View {
@Bindable var store: StoreOf<Settings>
// ...
}
If targeting iOS 16 or earlier, use @Perception.Bindable instead of @Bindable.
Create the binding
var body: some View {
Form {
Toggle (
"Haptic feedback" ,
isOn : $store. isHapticsEnabled . sending (\. isHapticsEnabledChanged )
)
}
}
Binding actions and reducers
For screens with many controls, creating individual actions for each binding can be tedious. The library provides BindableAction and BindingReducer to eliminate boilerplate.
The problem
Consider a settings screen with many editable fields:
@Reducer
struct 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!
}
}
}
This is a lot of boilerplate.
The solution
Conform action to BindableAction
@Reducer
struct Settings {
@ObservableState
struct State { /* ... */ }
enum Action : BindableAction {
case binding (BindingAction<State>)
}
// ...
}
Add BindingReducer
var body: some Reducer<State, Action> {
BindingReducer ()
}
Make store bindable in view
struct SettingsView : View {
@Bindable var store: StoreOf<Settings>
// ...
}
Derive bindings with $ syntax
var body: some View {
Form {
TextField ( "Display name" , text : $store. displayName )
Toggle ( "Notifications" , isOn : $store. enableNotifications )
Toggle ( "Email notifications" , isOn : $store. sendEmailNotifications )
Toggle ( "Mobile notifications" , isOn : $store. sendMobileNotifications )
// ...
}
}
That’s it! All the boilerplate is eliminated.
Observing specific bindings
You can layer additional functionality over bindings by pattern matching:
Pattern matching in reducer
var body: some Reducer<State, Action> {
BindingReducer ()
Reduce { state, action in
switch action {
case . binding (\. displayName ) :
// Validate display name
return . none
case . binding (\. enableNotifications ) :
// Request authorization from UNUserNotificationCenter
return . run { send in
let authorized = await requestNotificationAuthorization ()
await send (. notificationAuthorizationResponse (authorized))
}
default :
return . none
}
}
}
Using onChange
Alternatively, use onChange on the BindingReducer:
var body: some Reducer<State, Action> {
BindingReducer ()
. onChange ( of : \. displayName ) { oldValue, newValue in
Reduce { state, action in
// Validate display name
return . none
}
}
. onChange ( of : \. enableNotifications ) { oldValue, newValue in
Reduce { state, action in
// Return an authorization request effect
return . run { send in
let authorized = await requestNotificationAuthorization ()
await send (. notificationAuthorizationResponse (authorized))
}
}
}
}
Testing bindings
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.
Advanced: Custom bindings
You can create custom bindings that perform additional logic:
var volumeBinding: Binding< Double > {
$store. volume . sending { newValue in
// Custom validation or transformation
return . volumeChanged ( min ( max (newValue, 0 ), 1 ))
}
}
This allows you to intercept and transform values before they reach your reducer.
Best practices
Use BindingReducer for many bindings When you have 3+ bindable fields, use BindingReducer instead of ad hoc bindings
Validate with onChange Use onChange to add validation or side effects for specific fields
Test binding actions Always test binding actions to ensure proper state mutations
Keep logic in reducer Don’t put business logic in custom binding closures; keep it in the reducer
Common patterns
Conditional bindings
Sometimes you only want to update state when certain conditions are met:
var body: some Reducer<State, Action> {
BindingReducer ()
. onChange ( of : \. email ) { oldValue, newValue in
Reduce { state, action in
if newValue. contains ( "@" ) {
state. emailValid = true
} else {
state. emailValid = false
}
return . none
}
}
}
Derived bindings
You can derive bindings from computed properties:
extension SettingsView {
var notificationsEnabled: Binding< Bool > {
Binding (
get : { store. sendEmailNotifications && store. sendMobileNotifications },
set : { newValue in
store. send (. binding (. set (\. sendEmailNotifications , newValue)))
store. send (. binding (. set (\. sendMobileNotifications , newValue)))
}
)
}
}
Debounced bindings
For expensive operations, debounce binding changes:
var body: some Reducer<State, Action> {
BindingReducer ()
. onChange ( of : \. searchQuery ) { oldValue, newValue in
Reduce { state, action in
return . run { send in
try await Task. sleep ( for : . milliseconds ( 300 ))
await send (. performSearch (newValue))
}
. cancellable ( id : CancelID. search )
}
}
}
Troubleshooting
Binding not updating the view
Make sure:
Your store is marked with @Bindable (or @Perception.Bindable for iOS 16)
Your State is marked with @ObservableState
You’re using $store syntax to derive bindings
Compilation error with @Bindable
If targeting iOS 16 or earlier, use the backported version: @Perception . Bindable var store: StoreOf<Settings>
Verify that:
BindingReducer() is in your reducer’s body
Your action enum conforms to BindableAction
You have a case binding(BindingAction<State>) in your action enum