使用具有ForEach和可绑定语法的协议数组



我有一个@Published协议数组,我正在使用ForEach循环该数组,以在某些视图中显示元素。我希望能够使用SwiftUI可绑定语法和ForEach为我生成绑定,这样我就可以对每个元素进行变异,并将其反映在原始数组中。

这似乎适用于协议中实现的属性,但我不确定如何访问协议一致性类型特有的属性。在下面的示例代码中,这将是Animalowner属性或Humanage属性。我认为某种类型转换可能是必要的,但不知道如何通过绑定保留对底层数组的引用。

如果你需要更多细节,请告诉我。

import SwiftUI
protocol Testable {
var id: UUID { get }
var name: String { get set }
}
struct Human: Testable {
let id: UUID
var name: String
var age: Int
}
struct Animal: Testable {
let id: UUID
var name: String
var owner: String
}
class ContentViewModel: ObservableObject {
@Published var animalsAndHumans: [Testable] = []
}
struct ContentView: View {
@StateObject var vm: ContentViewModel = ContentViewModel()
var body: some View {
VStack {
ForEach($vm.animalsAndHumans, id: AnyTestable.id) { $object in
TextField("textfield", text: $object.name)
// if the object is an Animal, how can I get it's owner?
}
Button("Add animal") {
vm.animalsAndHumans.append(Animal(id: UUID(), name: "Mick", owner: "harry"))
}
Button("Add Human") {
vm.animalsAndHumans.append(Human(id: UUID(), name: "Ash", age: 26))
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

这是数据类型的一个棘手问题。

如果您可以更改数据类型,则可以更容易地解决此问题。

例如,也许您可以像这样对数据进行建模,使用enum而不是protocol来表示变体:

struct Testable {
let id: UUID
var name: String
var variant: Variant
enum Variant {
case animal(Animal)
case human(Human)
}
struct Animal {
var owner: String
}
struct Human {
var age: Int
}
}

它还将有助于为两种变体的相关数据添加访问者:

extension Testable {
var animal: Animal? {
get {
guard case .animal(let animal) = variant else { return nil }
return animal
}
set {
guard let newValue = newValue, case .animal(_) = variant else { return }
variant = .animal(newValue)
}
}
var human: Human? {
get {
guard case .human(let human) = variant else { return nil }
return human
}
set {
guard let newValue = newValue, case .human(_) = variant else { return }
variant = .human(newValue)
}
}
}

然后你可以这样写你的观点:

class ContentViewModel: ObservableObject {
@Published var testables: [Testable] = []
}
struct ContentView: View {
@StateObject var vm: ContentViewModel = ContentViewModel()
var body: some View {
VStack {
List {
ForEach($vm.testables, id: .id) { $testable in
VStack {
TextField("Name", text: $testable.name)
if let human = Binding($testable.human) {
Stepper("Age: (human.wrappedValue.age)", value: human.age)
}
else if let animal = Binding($testable.animal) {
HStack {
Text("Owner: ")
TextField("Owner", text: animal.owner)
}
}
}
}
}
HStack {
Button("Add animal") {
vm.testables.append(Testable(
id: UUID(),
name: "Mick",
variant: .animal(.init(owner: "harry"))
))
}
Button("Add Human") {
vm.testables.append(Testable(
id: UUID(),
name: "Ash",
variant: .human(.init(age: 26))
))
}
}
}
}
}

简单的解决方法是扩展Testable协议。像这样的东西

protocol Testable {
var id: UUID { get }
var name: String { get set }
var extra: String? { get }
}
struct Human: Testable {
let id: UUID
var name: String
var age: Int
var extra: String? { nil }
}
struct Animal: Testable {
let id: UUID
var name: String
var owner: String
var extra: String? { return owner }
}

您的ForEach块不需要知道具体类型:AnimalHuman,只需检查Testableextra即可决定是否添加新元素。

最新更新