I'm searching for a way to simplify/refactor the addition of .onChange(of:) in a SwiftUI view that has MANY TextFields. If a solution were concise, I would also move the modifier closer to the appropriate field rather than at the end of, say, a ScrollView. In this case, all of the .onChange modifiers call the same function.
Example:
.onChange(of: patientDetailVM.pubFirstName) { x in
changeBackButton()
}
.onChange(of: patientDetailVM.pubLastName) { x in
changeBackButton()
}
// ten+ more times for other fields
I tried "oring" the fields. This does not work:
.onChange(of:
patientDetailVM.pubFirstName ||
patientDetailVM.pubLastName
) { x in
changeBackButton()
}
This is the simple function that I want to call:
func changeBackButton() {
withAnimation {
showBackButton = false
isEditing = true
}
}
Any guidance would be appreciated. Xcode 13.2.1 iOS 15
We extend the Binding
type, to create two new methods, both of which are called onChange
.
Both onChange
methods are intended to be used in situations in which you need to perform some work whenever the Binding
instance's wrappedValue
property is changed (not just set) via its set
method.
The first onChange
method doesn't pass the new value of the Binding
instance's wrappedValue
property to the provided on-change callback method, whereas the second onChange
method does provide it with the new value.
The first onChange
method allows us to refactor this:
bindingToProperty.onChange { _ in
changeBackButton()
}
to this:
bindingToProperty.onChange(perform: changeBackButton)
import SwiftUI
extension Binding {
public func onChange(perform action: @escaping () -> Void) -> Self where Value : Equatable {
.init(
get: {
self.wrappedValue
},
set: { newValue in
guard self.wrappedValue != newValue else { return }
self.wrappedValue = newValue
action()
}
)
}
public func onChange(perform action: @escaping (_ newValue: Value) -> Void) -> Self where Value : Equatable {
.init(
get: {
self.wrappedValue
},
set: { newValue in
guard self.wrappedValue != newValue else { return }
self.wrappedValue = newValue
action(newValue)
}
)
}
}
struct EmployeeForm: View {
@ObservedObject var vm: VM
private func changeBackButton() {
print("changeBackButton method was called.")
}
private func occupationWasChanged() {
print("occupationWasChanged method was called.")
}
var body: some View {
Form {
TextField("First Name", text: $vm.firstName.onChange(perform: changeBackButton))
TextField("Last Name", text: $vm.lastName.onChange(perform: changeBackButton))
TextField("Occupation", text: $vm.occupation.onChange(perform: occupationWasChanged))
}
}
}
struct Person {
var firstName: String
var surname: String
var jobTitle: String
}
extension EmployeeForm {
class VM: ObservableObject {
@Published var firstName = ""
@Published var lastName = ""
@Published var occupation = ""
func load(from person: Person) {
firstName = person.firstName
lastName = person.surname
occupation = person.jobTitle
}
}
}
struct EditEmployee: View {
@StateObject private var employeeForm = EmployeeForm.VM()
@State private var isLoading = true
func fetchPerson() -> Person {
return Person(
firstName: "John",
surname: "Smith",
jobTitle: "Market Analyst"
)
}
var body: some View {
Group {
if isLoading {
Text("Loading...")
} else {
EmployeeForm(vm: employeeForm)
}
}
.onAppear {
employeeForm.load(from: fetchPerson())
isLoading = false
}
}
}
struct EditEmployee_Previews: PreviewProvider {
static var previews: some View {
EditEmployee()
}
}
Binding
instance is provided to the TextField/TextEditor/other type.Binding
instance that has a wrappedValue
property of any type that conforms to the Equatable
protocol.Binding
instances that have on-change callbacks, look just like Binding
instances that don't have on-change callbacks. Consequently, no types to which these Binding
instances with on-change callbacks are provided, need special modifications to know how to deal with them.View
's, @State
properties, ObservableObject
's, EnvironmentKey
's, PreferenceKey
's, or any other types. It simply adds a couple of methods to the existing type called Binding
- which obviously is a type that would have already been being used in the code...