我正在寻找一种方法来简化/重构在 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()
}
这样你就可以听到名字或姓氏的变化。 没有测试代码,只是作为一个想法。
我们扩展
Binding
类型,创建两个新方法,都称为onChange
.
这两种
onChange
方法都旨在用于当 Binding
实例的 wrappedValue
属性通过其 set
方法changed(不仅仅是 set)时需要执行某些工作的情况。
第一个
onChange
方法 doesn't 将 Binding
实例的 wrappedValue
属性的新值传递给提供的更改回调方法,而第二个 onChange
方法 does 为它提供新的价值。
第一个
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()
}
}
Binding
实例的地方。Binding
属性的wrappedValue
实例any类型符合Equatable
协议。Binding
具有更改回调的实例,看起来就像 Binding
没有更改回调的实例。因此,没有提供这些具有更改回调的Binding
实例的类型,需要特殊修改才能知道如何处理它们。View
、@State
属性、ObservableObject
、EnvironmentKey
、PreferenceKey
或任何其他类型。它只是向现有类型添加了几个方法,称为 Binding
- 这显然是一种已经在代码中使用的类型......这是我想出的一个相当干的方法。显然,一旦您编写了定义
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()
}
}
}
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: $0] }
}
}
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()
}
}
为什么不直接使用计算变量?
@State private var something: Int = 1
@State private var another: Bool = true
@State private var yetAnother: String = "whatever"
var anyOfMultiple: [String] {[
something.description,
another.description,
yetAnother
]}
var body: some View {
VStack {
//
}
.onChange(of: anyOfMultiple) { _ in
//
}
}