使用SwiftUI的ForEach来迭代任何遵循Identifiable协议的对象

5
在ViewModel中,我有:
public var models: [any Tabbable]

Tabable 以。。。开始

public protocol Tabbable: Identifiable {
   associatedtype Id
   var id: Id { get }
   /// ...
}

在我的ViewModel中,我可以使用Swift中的Array.forEach来:
models.forEach { _ in
   print("Test")
}

但是在SwiftUI中,我不能使用ForEach来:

ForEach(viewModel.models) { _ in
   Text("Test")
}

由于:

类型“任何可制表的”无法符合“可识别的”

有什么建议吗? 这是使用Swift 5.7。我需要回到制作类似AnyTabbable的东西吗? 提前致谢。


ForEach 无法为 ID 推断类型(我认为错误是由于编译器混淆导致的),因为存在双重泛型。如果您使用具体类型作为 id,则可以解决此问题。顺便说一下,在您的 forEach 语句中没有使用协议,因此比较是不正确的。 - Asperi
3个回答

6

考虑这个Playground形式的例子:

import UIKit
import SwiftUI

public protocol Tabbable: Identifiable {
    associatedtype Id
    var id: Id { get }
    
    var name : String { get }
}

struct TabbableA : Tabbable{
    typealias Id = Int
    
    var id : Id = 3
    var name = "TabbableA"
}

struct TabbableB : Tabbable {
    typealias Id = UUID
    
    var id : Id = UUID()
    var name = "TabbableB"
}

struct ViewModel {
    public var models: [any Tabbable]
}

let tabbableA = TabbableA()
let tabbableB = TabbableB()
let models: [any Tabbable] = [tabbableA, tabbableB]

struct ContentView {
    @State var viewModel : ViewModel = ViewModel(models: models)
    
    var body : some View {
        ForEach(viewModel.models) { model in
            Text(model.name)
        }
    }
}

在这种情况下,我们有类型TabbableA,其中每个实例都有一个整数类型的id属性(为了示例只使用“3”,但类型是重要的)。在TabbableB中,每个实例的id是UUID。然后我创建一个数组,其中一个项目是TabableA的实例,另一个是TabbableB的实例。该数组的类型为[任何可制表的]
然后我尝试在数组上使用ForEach。但是,数组中的一些元素使用Int id,而另一些元素使用UUID id。系统不知道要使用什么类型来唯一标识视图。IntsUUIDs不能直接相互比较以确定相等性。虽然进入数组的每个项目都是Tabbable,因此符合Identifiable,但是从数组中出来的元素,每个元素都是any Tabbable类型,不符合Identifiable。所以系统拒绝了我的代码。
另一种思考方式:类型为any SomeProtocol的值是一个可以包含符合SomeProtocol的任何内容的盒子,但是盒子本身不符合SomeProtocol

如果我想使“ID”类型具体化(即让编译器知道“[任何可选]”中的所有元素都具有类型为“UUID”的“ID”),这样是否可以解决此问题?如果是这样,我该如何更改代码以约束“Tabbable”的“ID”类型为“UUID”? - alobaili
这里有另一个达到了同样死胡同的线程:https://forums.swift.org/t/understanding-the-error-type-any-thing-cannot-conform-to-thing/60810 - alobaili
1
@alobaili 你可以将 Tabbable 声明为 public protocol Tabbable: Identifiable where ID == UUID,然后将 ForEach 更改为 ForEach(viewModel.models, id: \.id) - Scott Thompson
是的,在我的评论之后,我继续进行实验,并得出了相同的结论。我提供了一个记录我的结果的答案。感谢您的及时回复。 - alobaili
擦除模式与该问题相关吗?能帮助克服它吗? - Dinox
1
@Dinox 你可以使用某种类型擦除的方式来解决这个问题。基本上,你可以替换编译器提供的存在类型 any Tabbable,使用自己的一种“盒子”,该盒子具有一致的接口,并将其作为接口使用。在这个特定的例子中,该盒子必须具有比较 UUID 和整数的方法,但你是可以做到的。 - Scott Thompson

1

您可以在参数中提供KeyPath到id(或任何唯一变量),以指定如何在ForEach中检索ID。这是因为ForEach中的所有项目都必须是唯一的。您可以创建一个符合您协议的结构体。此时应该可以正常工作。第二个选项是从协议中删除Identifiable,然后您可以直接使用它。

    public protocol Tabbable {
        var id: String { get }
        /// ...
    }
    
    public var models: [Tabbable]

    ForEach(viewModel.models, id: \.id) { _ in
       Text("Test")
    }

或者

    public protocol TabbableType: Identifiable {
        associatedtype Id
        var id: Id { get }
        /// ...
    }

    struct Tabbable: TabbableType {
        var id: String { get }
        /// ...
    }

    public var models: [Tabbable]

    ForEach(viewModel.models) { _ in
       Text("Test")
    }

哪个出现了错误: 属性'id'要求'Self'是一个类类型这让我想到我需要创建一个AnyTabbable并进行经典的类型擦除。 - BrendanS
你可以创建一个符合你的协议的结构体,那样它应该能够工作。第二个选项是从协议中移除 Identifiable 然后你就可以直接使用它了。 - Đỗ Long Thành
要使用此实现,您确实需要使用类型擦除或Swift 5.7将自动为您执行此操作。您无法使用原始实现,因为ID类型不能保证相同,这是必需的比较。 - Steven

0

Scott的回答非常好地解释了编译器抛出该错误的原因。我正在尝试一种方法,以便为编译器提供必要的信息来使此代码编译,并找到了一个解决方案,但它需要向您的协议添加约束并扩展ForEach

您需要将协议的ID类型限制为具体的Hashable类型,例如UUID。这有非常特定的语法。如果您的用例取决于不同类型的ID,则可能会有缺点。

protocol Tabbable: Identifiable where ID == UUID {
    var id: Self.ID { get }
}

然后你需要扩展ForEach并给它一个初始化器,该初始化器具有所有必要的信息来为您推断id参数,同样,它具有非常特定的语法:

extension ForEach where ID == UUID, Content: View, Data.Element == any Tabbable {
    init(_ data: Data, @ViewBuilder content: @escaping (any Tabbable) -> Content) {
        self.init(data: id: \.id, content: content)
    }
}

然后,您将能够像预期的那样将类型为[any Tabbable]的值传递给ForEach。我不完全理解为什么需要这个扩展,但如果没有它,除非在初始化ForEach时显式传递id,否则代码将无法编译。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接