diff --git a/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-Bindings-Forms.swift b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-Bindings-Forms.swift index 1b533d020170..fabea577560b 100644 --- a/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-Bindings-Forms.swift +++ b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-Bindings-Forms.swift @@ -2,12 +2,12 @@ import ComposableArchitecture import SwiftUI private let readMe = """ - This file demonstrates how to handle two-way bindings in the Composable Architecture using form \ - actions. + This file demonstrates how to handle two-way bindings in the Composable Architecture using \ + binding actions. - Form actions allow you to eliminate the boilerplate caused by needing to have a unique action \ - for every UI control. Instead, all UI bindings can be consolidated into a single `form` action \ - that holds onto a `FormAction` value. + Binding actions allow you to eliminate the boilerplate caused by needing to have a unique action \ + for every UI control. Instead, all UI bindings can be consolidated into a single `binding` \ + action that holds onto a `BindingAction` value. It is instructive to compare this case study to the "Binding Basics" case study. """ @@ -21,7 +21,7 @@ struct BindingFormState: Equatable { } enum BindingFormAction: Equatable { - case form(FormAction) + case binding(BindingAction) case resetButtonTapped } @@ -32,11 +32,11 @@ let bindingFormReducer = Reducer< > { state, action, _ in switch action { - case .form(\.stepCount): + case .binding(\.stepCount): state.sliderValue = .minimum(state.sliderValue, Double(state.stepCount)) return .none - case .form: + case .binding: return .none case .resetButtonTapped: @@ -44,7 +44,7 @@ let bindingFormReducer = Reducer< return .none } } -.form(action: /BindingFormAction.form) +.binding(action: /BindingFormAction.binding) struct BindingFormView: View { let store: Store @@ -56,7 +56,7 @@ struct BindingFormView: View { HStack { TextField( "Type here", - text: viewStore.binding(keyPath: \.text, send: BindingFormAction.form) + text: viewStore.binding(keyPath: \.text, send: BindingFormAction.binding) ) .disableAutocorrection(true) .foregroundColor(viewStore.toggleIsOn ? .gray : .primary) @@ -64,12 +64,12 @@ struct BindingFormView: View { } .disabled(viewStore.toggleIsOn) - Toggle(isOn: viewStore.binding(keyPath: \.toggleIsOn, send: BindingFormAction.form)) { + Toggle(isOn: viewStore.binding(keyPath: \.toggleIsOn, send: BindingFormAction.binding)) { Text("Disable other controls") } Stepper( - value: viewStore.binding(keyPath: \.stepCount, send: BindingFormAction.form), + value: viewStore.binding(keyPath: \.stepCount, send: BindingFormAction.binding), in: 0...100 ) { Text("Max slider value: \(viewStore.stepCount)") @@ -81,7 +81,7 @@ struct BindingFormView: View { Text("Slider value: \(Int(viewStore.sliderValue))") .font(Font.body.monospacedDigit()) Slider( - value: viewStore.binding(keyPath: \.sliderValue, send: BindingFormAction.form), + value: viewStore.binding(keyPath: \.sliderValue, send: BindingFormAction.binding), in: 0...Double(viewStore.stepCount) ) } diff --git a/Examples/CaseStudies/SwiftUICaseStudiesTests/01-GettingStarted-BindingBasicsTests.swift b/Examples/CaseStudies/SwiftUICaseStudiesTests/01-GettingStarted-BindingBasicsTests.swift index b111aab3a4f0..5f4af26c7144 100644 --- a/Examples/CaseStudies/SwiftUICaseStudiesTests/01-GettingStarted-BindingBasicsTests.swift +++ b/Examples/CaseStudies/SwiftUICaseStudiesTests/01-GettingStarted-BindingBasicsTests.swift @@ -13,17 +13,17 @@ class BindingFormTests: XCTestCase { ) store.assert( - .send(.form(.set(\.sliderValue, 2))) { + .send(.binding(.set(\.sliderValue, 2))) { $0.sliderValue = 2 }, - .send(.form(.set(\.stepCount, 1))) { + .send(.binding(.set(\.stepCount, 1))) { $0.sliderValue = 1 $0.stepCount = 1 }, - .send(.form(.set(\.text, "Blob"))) { + .send(.binding(.set(\.text, "Blob"))) { $0.text = "Blob" }, - .send(.form(.set(\.toggleIsOn, true))) { + .send(.binding(.set(\.toggleIsOn, true))) { $0.toggleIsOn = true }, .send(.resetButtonTapped) { diff --git a/Sources/ComposableArchitecture/Internal/Deprecations.swift b/Sources/ComposableArchitecture/Internal/Deprecations.swift index 203969a7a7c1..aff7889b8141 100644 --- a/Sources/ComposableArchitecture/Internal/Deprecations.swift +++ b/Sources/ComposableArchitecture/Internal/Deprecations.swift @@ -1,6 +1,18 @@ import Combine import SwiftUI +// NB: Deprecated after 0.13.0: + +@available(*, deprecated, renamed: "BindingAction") +public typealias FormAction = BindingAction + +extension Reducer { + @available(*, deprecated, renamed: "binding") + public func form(action toFormAction: CasePath>) -> Self { + self.binding(action: toFormAction) + } +} + // NB: Deprecated after 0.10.0: @available(iOS 13, *) diff --git a/Sources/ComposableArchitecture/SwiftUI/Forms.swift b/Sources/ComposableArchitecture/SwiftUI/Binding.swift similarity index 69% rename from Sources/ComposableArchitecture/SwiftUI/Forms.swift rename to Sources/ComposableArchitecture/SwiftUI/Binding.swift index dcf327b62e93..95474b394825 100644 --- a/Sources/ComposableArchitecture/SwiftUI/Forms.swift +++ b/Sources/ComposableArchitecture/SwiftUI/Binding.swift @@ -64,49 +64,49 @@ import SwiftUI /// } /// /// This is a _lot_ of boilerplate for something that should be simple. Luckily, we can dramatically -/// eliminate this boilerplate using `FormAction`. First, we can collapse all of these -/// field-mutating actions into a single case that holds a `FormAction` generic over the reducer's -/// root `SettingsState`: +/// eliminate this boilerplate using `BindingAction`. First, we can collapse all of these +/// field-mutating actions into a single case that holds a `BindingAction` generic over the +/// reducer's root `SettingsState`: /// /// enum SettingsAction { -/// case form(FormAction) +/// case binding(BindingAction) /// } /// -/// And then, we can simplify the settings reducer by allowing the `form` method to handle these +/// And then, we can simplify the settings reducer by allowing the `binding` method to handle these /// field mutations for us: /// /// let settingsReducer = Reducer< /// SettingsState, SettingsAction, SettingsEnvironment /// > { /// switch action { -/// case .form: +/// case .binding: /// return .none /// } /// } -/// .form(action: /SettingsAction.form) +/// .binding(action: /SettingsAction.binding) /// -/// Form actions are constructed and sent to the store by providing a writable key path from root +/// Binding actions are constructed and sent to the store by providing a writable key path from root /// state to the field being mutated. There is even a view store helper that simplifies this work. -/// You can derive a binding by specifying the key path and form action case: +/// You can derive a binding by specifying the key path and binding action case: /// /// TextField( /// "Display name", -/// text: viewStore.binding(keyPath: \.displayName, send: SettingsAction.form) +/// text: viewStore.binding(keyPath: \.displayName, send: SettingsAction.binding) /// ) /// -/// Should you need to layer additional functionality over your form, your reducer can pattern match -/// the form action for a given key path: +/// Should you need to layer additional functionality over these bindings, your reducer can pattern +/// match the action for a given key path: /// -/// case .form(\.displayName): +/// case .binding(\.displayName): /// // Validate display name /// -/// case .form(\.enableNotifications): +/// case .binding(\.enableNotifications): /// // Return an authorization request effect /// -/// Form actions can also be tested in much the same way regular actions are tested. Rather than +/// Binding actions can also be tested in much the same way regular actions are tested. Rather than /// send a specific action describing how a binding changed, such as `displayNameChanged("Blob")`, -/// you will send a `.form` action that describes which key path is being set to what value, such -/// as `.form(.set(\.displayName, "Blob"))`: +/// you will send a `.binding` action that describes which key path is being set to what value, such +/// as `.binding(.set(\.displayName, "Blob"))`: /// /// let store = TestStore( /// initialState: SettingsState(), @@ -115,15 +115,15 @@ import SwiftUI /// ) /// /// store.assert( -/// .send(.form(.set(\.displayName, "Blob"))) { +/// .send(.binding(.set(\.displayName, "Blob"))) { /// $0.displayName = "Blob" /// }, -/// .send(.form(.set(\.protectMyPosts, true))) { +/// .send(.binding(.set(\.protectMyPosts, true))) { /// $0.protectMyPosts = true /// ) /// ) /// -public struct FormAction: Equatable { +public struct BindingAction: Equatable { public let keyPath: PartialKeyPath fileprivate let set: (inout Root) -> Void @@ -150,12 +150,14 @@ public struct FormAction: Equatable { ) } - /// Transforms a form action over some root state to some other type of root state given a key + /// Transforms a binding action over some root state to some other type of root state given a key /// path. /// /// - Parameter keyPath: A key path from a new type of root state to the original root state. - /// - Returns: A form action over a new type of root state. - public func pullback(_ keyPath: WritableKeyPath) -> FormAction { + /// - Returns: A binding action over a new type of root state. + public func pullback( + _ keyPath: WritableKeyPath + ) -> BindingAction { .init( keyPath: (keyPath as AnyKeyPath).appending(path: self.keyPath) as! PartialKeyPath, set: { self.set(&$0[keyPath: keyPath]) }, @@ -170,38 +172,39 @@ public struct FormAction: Equatable { public static func ~= ( keyPath: WritableKeyPath, - formAction: FormAction + bindingAction: Self ) -> Bool { - keyPath == formAction.keyPath + keyPath == bindingAction.keyPath } } extension Reducer { - /// Returns a reducer that applies `FormAction` mutations to `State` before running this reducer's - /// logic. + /// Returns a reducer that applies `BindingAction` mutations to `State` before running this + /// reducer's logic. /// - /// For example, a settings screen may gather its form actions into a single `FormAction` case: + /// For example, a settings screen may gather its binding actions into a single `BindingAction` + /// case: /// /// enum SettingsAction { /// ... - /// case form(FormAction) + /// case binding(BindingAction) /// } /// /// The reducer can then be enhanced to automatically handle these mutations for you by tacking on - /// the `form` method: + /// the `binding` method: /// /// let settingsReducer = Reducer>) -> Self { + public func binding(action toBindingAction: CasePath>) -> Self { Self { state, action, environment in - toFormAction.extract(from: action)?.set(&state) + toBindingAction.extract(from: action)?.set(&state) return .none } .combined(with: self) @@ -210,25 +213,25 @@ extension Reducer { extension ViewStore { /// Derives a binding from the store that mutates state at the given writable key path by wrapping - /// a `FormAction` with the store's action type. + /// a `BindingAction` with the store's action type. /// /// For example, a text field binding can be created like this: /// /// struct State { var text = "" } - /// enum Action { case form(FormAction) } + /// enum Action { case binding(BindingAction) } /// /// TextField( /// "Enter text", - /// text: viewStore.binding(keyPath: \.text, Action.form) + /// text: viewStore.binding(keyPath: \.text, Action.binding) /// ) /// /// - Parameters: /// - keyPath: A writable key path from the view store's state to a mutable field - /// - action: A function that wraps a form action in the view store's action type. + /// - action: A function that wraps a binding action in the view store's action type. /// - Returns: A binding. public func binding( keyPath: WritableKeyPath, - send action: @escaping (FormAction) -> Action + send action: @escaping (BindingAction) -> Action ) -> Binding where LocalState: Equatable { self.binding(