ios SwiftUI Simplify .onChange多个文本字段的修饰符

hmae6n7t  于 2023-03-09  发布在  iOS
关注(0)|答案(5)|浏览(119)

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

我试过用“O”法。但没用:

.onChange(of:
            patientDetailVM.pubFirstName ||
            patientDetailVM.pubLastName
) { x in
    changeBackButton()
}

这是我要调用的简单函数:

func changeBackButton() {
    withAnimation {
        showBackButton = false
        isEditing = true
    }
}

任何指导将不胜感激。Xcode 13.2.1 iOS 15

dnph8jn4

dnph8jn41#

每当您复制代码时,您都希望将其向下移动一级,以便可以重用相同的代码。
这里有一个解决方案,父视图将保存一个变量,该变量将知道“名称”作为一个整体是否发生了变化。

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

h6my8fg22#

另一种方法是为pubFirstNamepubLastName创建一个组合发布器。

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 publisher

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

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

5kgi1eie

5kgi1eie3#

解决方案概述

我们扩展了Binding类型,以创建两个新方法,这两个方法都称为onChange
这两个onChange方法都是为了在这样的情况下使用的:当Binding示例的wrappedValue属性通过其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都很简单,并且保持在最小。
    1.它使onChange-callback非常靠近Binding示例提供给TextField/TextEditor/other类型的位置。
    1.它是通用的,而且用途非常广泛,因为它可以用于任何Binding示例,只要该示例具有符合Equatable协议的任何类型的wrappedValue属性。
    1.具有更改时回调的Binding示例看起来就像没有更改时回调的Binding示例一样。因此,不需要对这些具有更改时回调的Binding示例所提供的类型进行特殊修改以了解如何处理它们。
  2. helper-code并不涉及创建任何新的View@State属性、ObservableObjectEnvironmentKeyPreferenceKey或任何其他类型,它只是在现有的Binding类型中添加了几个方法--很明显,该类型已经在代码中使用过了......
vuv7lop3

vuv7lop34#

这是我想到的一个相当枯燥的方法,显然,一旦你写好了定义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()
    }
}
vbkedwbf

vbkedwbf5#

为什么不直接使用计算的var呢?

@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
        //
    }
}

相关问题