SwiftUI 简化许多文本字段的 .onChange 修饰符

问题描述 投票:0回答:5

我正在寻找一种方法来简化/重构在 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

ios xcode swiftui
5个回答
2
投票

每当你复制代码时,你都想将它向下移动一层,以便可以重复使用相同的代码。

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

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

2
投票

您可以这样做的另一种方法是为

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

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


1
投票

解决方案概述

我们扩展

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

解决方案的好处

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

1
投票

这是我想出的一个相当干的方法。显然,一旦您编写了定义

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

0
投票

为什么不直接使用计算变量?

@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
        //
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.