SwiftUI 简化许多 TextFields 的 .onChange 修饰符
SwiftUI Simplify .onChange Modifier for Many TextFields
我正在寻找一种方法 simplify/refactor 在 SwiftUI 中添加 .onChange(of:)
具有许多 TextField 的视图。如果解决方案简洁,我也会移动修饰符
更接近适当的字段,而不是在 ScrollView 的末尾。在这个
在这种情况下,所有 .onChange 修饰符都调用相同的函数。
示例:
.onChange(of: patientDetailVM.pubFirstName) { x in
changeBackButton()
}
.onChange(of: patientDetailVM.pubLastName) { x in
changeBackButton()
}
// ten+ more times for other fields
我试过“oring”字段。这不起作用:
.onChange(of:
patientDetailVM.pubFirstName ||
patientDetailVM.pubLastName
) { x in
changeBackButton()
}
这是我要调用的简单函数:
func changeBackButton() {
withAnimation {
showBackButton = false
isEditing = true
}
}
如有任何指导,我们将不胜感激。 Xcode 13.2.1 iOS 15
任何时候复制代码时,您都希望将其向下移动一层,以便重复使用相同的代码。
这是一个解决方案,父视图将保存一个变量,该变量将知道“名称”作为一个整体是否发生了变化。
import SwiftUI
class PatientDetailViewModel: ObservableObject{
@Published var pubFirstName: String = "John"
@Published var pubLastName: String = "Smith"
}
struct TrackingChangesView: View {
@StateObject var vm: PatientDetailViewModel = PatientDetailViewModel()
///Variable to know if there is a change
@State var nameHasChanges: Bool = false
var body: some View {
NavigationView{
NavigationLink("EditView", destination: {
VStack{
TrackingChangesTextFieldView(hasChanges: $nameHasChanges, text: $vm.pubFirstName, titleKey: "first name")
TrackingChangesTextFieldView(hasChanges: $nameHasChanges, text: $vm.pubLastName, titleKey: "last name")
Button("save", action: {
//Once you verify saving the object reset the variable
nameHasChanges = false
})//Enable button when there are changes
.disabled(!nameHasChanges)
}
//Or track the single variable here
.onChange(of: nameHasChanges, perform: {val in
//Your method here
})
//trigger back button with variable
.navigationBarBackButtonHidden(nameHasChanges)
})
}
}
}
struct TrackingChangesTextFieldView: View {
//Lets the parent view know that there has been a change
@Binding var hasChanges: Bool
@Binding var text: String
let titleKey: String
var body: some View {
TextField(titleKey, text: $text)
.onChange(of: text, perform: { _ in
//To keep it from reloading view if already true
if !hasChanges{
hasChanges = true
}
})
}
}
struct TrackingChangesView_Previews: PreviewProvider {
static var previews: some View {
TrackingChangesView()
}
}
另一种方法是为 pubFirstName
和 pubLastName
创建一个组合发布者。
将以下功能添加到您的 viewModel
var nameChanged: AnyPublisher<Bool, Never> {
$patientDetailVM.pubFirstName
.combineLatest($patientDetailVM.pubLastName)
.map { firstName, lastName in
if firstName != patientDetailVM.pubFirstName ||
lastName != patientDetailVM.pubLastName
{
return true
} else {
return false
}
}
.eraseToAnyPublisher()
}
并听取 nameChanged
发布者关于 onReceive
您的看法
.onReceive(of: patientDetailVM.nameChanged) { hasNameChanged in
changeBackButton()
}
因此您可以收听名字或姓氏的更改。
没有测试代码,只是作为一个想法。
这是我想出的一个相当枯燥的方法。显然,一旦您编写了定义 NameKeyPathPairs
结构的代码,以及对 Array
的扩展等,使用起来就非常简单。
用法示例
import SwiftUI
struct EmployeeForm: View {
@ObservedObject var vm: VM
private let textFieldProps: NameKeyPathPairs<String, ReferenceWritableKeyPath<VM, String>> = [
"First Name": \.firstName,
"Last Name": \.lastName,
"Occupation": \.occupation
]
private func changeBackButton() {
print("changeBackButton method was called.")
}
var body: some View {
Form {
ForEach(textFieldProps, id: \.name) { (name, keyPath) in
TextField(name, text: $vm[dynamicMember: keyPath])
}
}
.onChange(of: textFieldProps.keyPaths.applied(to: vm)) { _ in
changeBackButton()
}
}
}
.onChange 辅助代码
public struct NameKeyPathPairs<Name, KP>: ExpressibleByDictionaryLiteral where Name : ExpressibleByStringLiteral, KP : AnyKeyPath {
private let data: [Element]
public init(dictionaryLiteral elements: (Name, KP)...) {
self.data = elements
}
public var names: [Name] {
map(\.name)
}
public var keyPaths: [KP] {
map(\.keyPath)
}
}
extension NameKeyPathPairs : Sequence, Collection, RandomAccessCollection {
public typealias Element = (name: Name, keyPath: KP)
public typealias Index = Array<Element>.Index
public var startIndex: Index { data.startIndex }
public var endIndex: Index { data.endIndex }
public subscript(position: Index) -> Element { data[position] }
}
extension RandomAccessCollection {
public func applied<Root, Value>(to root: Root) -> [Value] where Element : KeyPath<Root, Value> {
map { root[keyPath: [=11=]] }
}
}
示例的剩余代码
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
类型,创建了两个新方法,它们都被称为 onChange
.
这两种 onChange
方法都旨在用于需要在 Binding
实例的 wrappedValue
属性 为 [=59= 时执行某些工作的情况]更改(不仅仅是设置)通过它的set
方法。
第一个 onChange
方法 不会 将 Binding
实例的 wrappedValue
属性 的新值传递给提供了 on-change 回调方法,而第二个 onChange
方法 确实 为它提供了新值。
第一个 onChange
方法允许我们重构它:
bindingToProperty.onChange { _ in
changeBackButton()
}
对此:
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()
}
}
解决方案的好处
- 帮助程序代码和使用代码都很简单,而且保持非常小。
- 它使 onChange-callback 非常 靠近
Binding
实例提供给 TextField/TextEditor/other 类型的地方。
- 它是通用的,并且非常通用,因为它可以用于任何具有
wrappedValue
属性 的 符合 Equatable
协议的任何 类型。
Binding
个具有更改回调的实例,看起来就像 Binding
个没有更改回调的实例。因此,没有提供这些 Binding
具有更改回调的实例的类型,需要特殊修改才能知道如何处理它们。
- 帮助程序代码不涉及创建任何新的
View
、@State
属性、ObservableObject
、EnvironmentKey
、PreferenceKey
's,或任何其他类型。它只是向名为 Binding
的现有类型添加了几个方法 - 这显然是一种已经在代码中使用的类型...
查看此问题的 twoStraws 解决方案 - 添加绑定协议的扩展 - 允许您向任何 state/binding 变量添加修饰符并在使用该变量时调用函数。很干净。
我正在寻找一种方法 simplify/refactor 在 SwiftUI 中添加 .onChange(of:) 具有许多 TextField 的视图。如果解决方案简洁,我也会移动修饰符 更接近适当的字段,而不是在 ScrollView 的末尾。在这个 在这种情况下,所有 .onChange 修饰符都调用相同的函数。
示例:
.onChange(of: patientDetailVM.pubFirstName) { x in
changeBackButton()
}
.onChange(of: patientDetailVM.pubLastName) { x in
changeBackButton()
}
// ten+ more times for other fields
我试过“oring”字段。这不起作用:
.onChange(of:
patientDetailVM.pubFirstName ||
patientDetailVM.pubLastName
) { x in
changeBackButton()
}
这是我要调用的简单函数:
func changeBackButton() {
withAnimation {
showBackButton = false
isEditing = true
}
}
如有任何指导,我们将不胜感激。 Xcode 13.2.1 iOS 15
任何时候复制代码时,您都希望将其向下移动一层,以便重复使用相同的代码。
这是一个解决方案,父视图将保存一个变量,该变量将知道“名称”作为一个整体是否发生了变化。
import SwiftUI
class PatientDetailViewModel: ObservableObject{
@Published var pubFirstName: String = "John"
@Published var pubLastName: String = "Smith"
}
struct TrackingChangesView: View {
@StateObject var vm: PatientDetailViewModel = PatientDetailViewModel()
///Variable to know if there is a change
@State var nameHasChanges: Bool = false
var body: some View {
NavigationView{
NavigationLink("EditView", destination: {
VStack{
TrackingChangesTextFieldView(hasChanges: $nameHasChanges, text: $vm.pubFirstName, titleKey: "first name")
TrackingChangesTextFieldView(hasChanges: $nameHasChanges, text: $vm.pubLastName, titleKey: "last name")
Button("save", action: {
//Once you verify saving the object reset the variable
nameHasChanges = false
})//Enable button when there are changes
.disabled(!nameHasChanges)
}
//Or track the single variable here
.onChange(of: nameHasChanges, perform: {val in
//Your method here
})
//trigger back button with variable
.navigationBarBackButtonHidden(nameHasChanges)
})
}
}
}
struct TrackingChangesTextFieldView: View {
//Lets the parent view know that there has been a change
@Binding var hasChanges: Bool
@Binding var text: String
let titleKey: String
var body: some View {
TextField(titleKey, text: $text)
.onChange(of: text, perform: { _ in
//To keep it from reloading view if already true
if !hasChanges{
hasChanges = true
}
})
}
}
struct TrackingChangesView_Previews: PreviewProvider {
static var previews: some View {
TrackingChangesView()
}
}
另一种方法是为 pubFirstName
和 pubLastName
创建一个组合发布者。
将以下功能添加到您的 viewModel
var nameChanged: AnyPublisher<Bool, Never> {
$patientDetailVM.pubFirstName
.combineLatest($patientDetailVM.pubLastName)
.map { firstName, lastName in
if firstName != patientDetailVM.pubFirstName ||
lastName != patientDetailVM.pubLastName
{
return true
} else {
return false
}
}
.eraseToAnyPublisher()
}
并听取 nameChanged
发布者关于 onReceive
您的看法
.onReceive(of: patientDetailVM.nameChanged) { hasNameChanged in
changeBackButton()
}
因此您可以收听名字或姓氏的更改。 没有测试代码,只是作为一个想法。
这是我想出的一个相当枯燥的方法。显然,一旦您编写了定义 NameKeyPathPairs
结构的代码,以及对 Array
的扩展等,使用起来就非常简单。
用法示例
import SwiftUI
struct EmployeeForm: View {
@ObservedObject var vm: VM
private let textFieldProps: NameKeyPathPairs<String, ReferenceWritableKeyPath<VM, String>> = [
"First Name": \.firstName,
"Last Name": \.lastName,
"Occupation": \.occupation
]
private func changeBackButton() {
print("changeBackButton method was called.")
}
var body: some View {
Form {
ForEach(textFieldProps, id: \.name) { (name, keyPath) in
TextField(name, text: $vm[dynamicMember: keyPath])
}
}
.onChange(of: textFieldProps.keyPaths.applied(to: vm)) { _ in
changeBackButton()
}
}
}
.onChange 辅助代码
public struct NameKeyPathPairs<Name, KP>: ExpressibleByDictionaryLiteral where Name : ExpressibleByStringLiteral, KP : AnyKeyPath {
private let data: [Element]
public init(dictionaryLiteral elements: (Name, KP)...) {
self.data = elements
}
public var names: [Name] {
map(\.name)
}
public var keyPaths: [KP] {
map(\.keyPath)
}
}
extension NameKeyPathPairs : Sequence, Collection, RandomAccessCollection {
public typealias Element = (name: Name, keyPath: KP)
public typealias Index = Array<Element>.Index
public var startIndex: Index { data.startIndex }
public var endIndex: Index { data.endIndex }
public subscript(position: Index) -> Element { data[position] }
}
extension RandomAccessCollection {
public func applied<Root, Value>(to root: Root) -> [Value] where Element : KeyPath<Root, Value> {
map { root[keyPath: [=11=]] }
}
}
示例的剩余代码
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
类型,创建了两个新方法,它们都被称为 onChange
.
这两种 onChange
方法都旨在用于需要在 Binding
实例的 wrappedValue
属性 为 [=59= 时执行某些工作的情况]更改(不仅仅是设置)通过它的set
方法。
第一个 onChange
方法 不会 将 Binding
实例的 wrappedValue
属性 的新值传递给提供了 on-change 回调方法,而第二个 onChange
方法 确实 为它提供了新值。
第一个 onChange
方法允许我们重构它:
bindingToProperty.onChange { _ in
changeBackButton()
}
对此:
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()
}
}
解决方案的好处
- 帮助程序代码和使用代码都很简单,而且保持非常小。
- 它使 onChange-callback 非常 靠近
Binding
实例提供给 TextField/TextEditor/other 类型的地方。 - 它是通用的,并且非常通用,因为它可以用于任何具有
wrappedValue
属性 的 符合Equatable
协议的任何 类型。 Binding
个具有更改回调的实例,看起来就像Binding
个没有更改回调的实例。因此,没有提供这些Binding
具有更改回调的实例的类型,需要特殊修改才能知道如何处理它们。- 帮助程序代码不涉及创建任何新的
View
、@State
属性、ObservableObject
、EnvironmentKey
、PreferenceKey
's,或任何其他类型。它只是向名为Binding
的现有类型添加了几个方法 - 这显然是一种已经在代码中使用的类型...
查看此问题的 twoStraws 解决方案 - 添加绑定协议的扩展 - 允许您向任何 state/binding 变量添加修饰符并在使用该变量时调用函数。很干净。