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