【问题标题】:SwiftUI Simplify .onChange Modifier for Many TextFieldsSwiftUI 为许多文本字段简化 .onChange 修饰符
【发布时间】:2022-01-21 17:52:13
【问题描述】:

我正在寻找一种方法来简化/重构 .onChange(of:) 在 SwiftUI 中的添加 具有许多文本字段的视图。如果解决方案简洁,我也会移动修饰符 更接近适当的字段,而不是在 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

【问题讨论】:

    标签: ios xcode swiftui


    【解决方案1】:

    任何时候复制代码时,您都希望将其下移一级,以便重复使用相同的代码。

    这是一个解决方案,父视图将保存一个变量,该变量将知道“名称”作为一个整体是否发生了变化。

    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()
        }
    }
    

    【讨论】:

    • 谢谢。这确实有效。我不确定它是否简单得多,但我会进行实验。同样,对于其他人 - 试试这个。
    • @JohnSF 它大多只是看起来更干净,并且不会在用户每次输入字母或进行更正时调用该方法。此外,如果您更改文本字段的外观,您只需在 View 与单个 TextFields 中进行更改。此外,您总体上维护的代码更少。
    【解决方案2】:

    您可以这样做的另一种方法是为pubFirstNamepubLastName 创建一个组合发布者。 将以下功能添加到您的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()
        }
    

    并在onReceive 上聆听nameChanged 发布者您的观点

    .onReceive(of: patientDetailVM.nameChanged) { hasNameChanged in
        changeBackButton()
    }
    
    

    这样您就可以听到名字或姓氏的变化。 没有测试代码,只是作为一个想法。

    【讨论】:

    • 这看起来是一个有趣的解决方案,但它并不能像所写的那样工作。我会玩一下。
    【解决方案3】:

    这是我想出的一种相当干燥的方法。显然,一旦编写了定义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: $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()
        }
    }
    

    【讨论】:

    • 有趣 - 再次有用。
    • 嗨@JohnSF,我刚刚为这个问题添加了一个额外的答案,我比我在这里的答案更喜欢这个答案。请让我知道您对此的看法:)。
    【解决方案4】:

    解决方案概述

    我们扩展Binding 类型,创建两个新方法,都称为onChange

    onChange 两种方法都适用于当Binding 实例的wrappedValue 属性更改(不仅仅是set) 通过其set 方法。

    第一个onChange 方法Binding 实例的wrappedValue 属性的新值传递给提供的更改回调方法,而第二个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()
        }
    }
    

    解决方案的好处

    1. helper-code 和usage-code 都很简单,而且非常少。
    2. 它使 onChange-callback 非常靠近将 Binding 实例提供给 TextField/TextEditor/其他类型的位置。
    3. 它是通用,并且用途广泛,因为它可以用于任何Binding 实例,其wrappedValue 属性为any 符合Equatable 协议的类型。
    4. 具有 on-change 回调的 Binding 实例看起来就像没有 on-change 回调的 Binding 实例。因此,没有为这些带有 on-change 回调的 Binding 实例提供任何类型,需要特殊修改才能知道如何处理它们。
    5. 帮助程序代码不涉及创建任何新的View@State 属性、ObservableObjectEnvironmentKeyPreferenceKey 或任何其他类型。它只是在现有类型Binding 中添加了几个方法——这显然是一种已经在代码中使用的类型...

    【讨论】:

    • 嘿杰里米,我喜欢这个。我会在早上试一试。是的,我喜欢让回调靠近现场。
    • @JohnSF,听起来不错:)!
    • 嗨@JohnSF,你有机会尝试一下吗?如果是这样,它对你的效果如何:)?
    • 嘿杰里米。我用 TextFields 和 TextEditors 做了一个简单的实验,你的解决方案效果很好。我还没有转换我的应用程序 - 很多字段要转换,但我计划分支 git 并这样做。再次感谢。
    猜你喜欢
    • 2021-06-12
    • 2020-01-30
    • 2021-03-19
    • 2022-09-25
    • 2021-11-18
    • 1970-01-01
    • 2021-10-04
    • 2022-06-28
    • 2019-12-29
    相关资源
    最近更新 更多