Swift类型 ‘any View’ 无法符合具有协议的 ‘View’。

huangapple go评论68阅读模式
英文:

Swift type 'any View' cannot conform to 'View' with protocol

问题

可能已经有类似的问题了,但似乎大部分都与我所问的“不太一样”。我来自Objective-C背景,那里很容易将对象声明为符合协议并直接将消息传递给符合协议的类型。对我来说,Swift类型系统似乎过于复杂,但我正在努力学习。

我正在构建一个通用的“选项卡视图”,在屏幕顶部显示多个选项卡,并且每个选项卡都有自己的“内容视图”,当选项卡处于“活动状态”时,会显示在下方。我遇到的问题与协议类型有关。我有以下协议:

protocol TabItem: Identifiable {
    associatedtype ContentView: View

    var title: String { get }
    var id: UUID { get }
    var contentView: ContentView { get }
}

还有一个可以显示选项卡本身的视图(以前,这是一个带有 <T : TabItem> 的通用视图,用于 item 属性,但我在这里遇到了更多的协议问题):

struct TabView: View {
    var item: any TabItem

    var body: some View {
        /* 在这里实现选项卡视图 */
    }
}

最后,我有一个显示选项卡及其内容的视图,以下对我来说非常直观,但它给我一个晦涩的错误消息:“类型 'any View' 无法符合 View”:

struct TabbedView: View {
    var items: [any TabItem]
    var activeItem: (any TabItem)?

    var body: some View {
        ForEach(self.items, id: \.id) { item in
            TabView(item: item)
        }

        Divider()

        if let activeItem = self.activeItem {
            // 这里出现错误
            activeItem.contentView
        }
    }
}

在保持通用性的同时,我该如何解决这个问题?我想能够在各种实现了 TabItem 协议的类中为 ContentView 放入任何视图。我理解编译器可能希望能够严格检查类型或类似的事情,但似乎为了实现这一点,进行了巨大的功能牺牲?我应该使用我不知道的某种特定模式吗?

英文:

There probably has been a question similar to this, but it seems most of them are "not quite" the same as what I'm asking. I'm coming from an Objective-C background, where it's trivial to declare objects as conforming to protocols and directly passing messages to protocol-conforming types. The Swift type system to me seems extremely overly complicated, but alas I'm trying to learn.

I'm building a generic "tabbed view" where a number of tabs is displayed at the top of the screen, and each tab has it's own "content view" which is displayed below when the tab is "active." The problem I'm running into is something to do with protocol types. I have the following protocol:

protocol TabItem : Identifiable
{
    associatedtype ContentView: View

    var title: String { get }

    var id: UUID { get }

    var contentView: ContentView { get }
}

And the a view which can display the tab itself (previously, this was a generic view with <T : TabItem> for the item property, but I ran into more protocol issues here:

struct TabView : View
{
    var item: any TabItem

    var body : some View {
        /* Tab view implementation here */
    }
}

Finally, I have the view which displays the tabs and their content, and the following is very intuitive to me, but it gives me a cryptic error message of Type 'any View' cannot conform to View:

struct TabbedView : View
{
    var items: [any TabItem]
    var activeItem: (any TabItem)?

    var body : some View {
        ForEach(self.items, id: \.id) { item in
            TabView(item: item)
        }

        Divider()

        if let activeItem = self.activeItem {
            // The error appears here
            activeItem.contentView
        }
    }
}

How can I fix this while keeping it generic? I want to be able to put any view in ContentView for various classes which implement the TabItem protocol. I understand the compiler likely wants to be able to strictly type check things or something like this, but it seems like there's been a huge functionality sacrifice to achieve this? Is there some specific pattern I should be using that I don't know about?

答案1

得分: 1

Here is the translation of the code part you provided:

在这里有您提供的代码的翻译:

虽然简单的解决方案是只需将 `ContentView` 包装在 `AnyView` 中...

```swift
// 在 TabItem 中
var contentAsAnyView: AnyView { AnyView(contentView) }
// 在 TabbedView 的主体中使用上述内容

最好避免使用 AnyView

让我们看看现有的 SwiftUI TabView 是如何工作的。您应该将所有选项卡的内容放入视图生成器中

TabView {
    ReceivedView()
        .badge(2)
        .tabItem {
            Label("Received", systemImage: "tray.and.arrow.down.fill")
        }
    SentView()
        .tabItem {
            Label("Sent", systemImage: "tray.and.arrow.up.fill")
        }
    AccountView()
        .badge("!")
        .tabItem {
            Label("Account", systemImage: "person.crop.circle.fill")
        }
}

这里的关键是,通过使用视图生成器,TabView 避免了需要将所有选项卡的类型作为其类型参数。无论视图生成器构建什么视图,都将是 TabViewContent 类型参数的类型。

我们可以做类似的事情,除了我们不知道有多少个选项卡(请参阅此处以了解 SwiftUI 如何做到这一点),因此我们将需要用户传递一个“选项卡标识符”数组,然后要求用户为每个选项卡提供一个标签 View

struct TabbedView<Selection: Hashable, Body: View>: View {
    
    let tabs: [Selection]
    @State var selection: Selection
    
    let computeView: (Selection) -> Body
    
    var body: some View {
        VStack {
            // 用于选择选项卡的简单视图
            Picker("Tabs", selection: $selection) {
                ForEach(tabs, id: \.self) { tab in
                    // 用您自定义的选项卡项视图替换此处
                    Text(String(describing: tab))
                    // 还考虑删除 Selection 类型参数
                    // 并使用一个具体类型,从中可以制作选项卡项视图
                }
            }.pickerStyle(.segmented)

            computeView(selection)
        }
    }
    
    init(tabs: [Selection], @ViewBuilder computeView: @escaping (Selection) -> Body) {
        self.tabs = tabs
        self._selection = State(initialValue: tabs.first!)
        self.computeView = computeView
    }
}

用法:

enum Tabs: Int, CaseIterable {
    case tab1, tab2, tab3
}

TabbedView(tabs: Tabs.allCases) { selection in
    switch selection {
    case .tab1:
        Text("Tab 1")
    case .tab2:
        Rectangle().foregroundColor(.blue)
    case .tab3:
        Button("Button") { }
    }
}

请注意,代码部分已经被翻译成中文。如果需要进一步的翻译或有其他问题,请告诉我。

<details>
<summary>英文:</summary>

While the simple solution is to just wrap the `ContentView` in an `AnyView`...

// in TabItem
var contentAsAnyView: AnyView { AnyView(contentView) }
// use the above in the body of TabbedView


[It is better to avoid `AnyView`][1].

Let&#39;s see how the existing SwiftUI [`TabView`][2] works. You are supposed to put all the tabs&#39; contents into a view builder

TabView {
ReceivedView()
.badge(2)
.tabItem {
Label("Received", systemImage: "tray.and.arrow.down.fill")
}
SentView()
.tabItem {
Label("Sent", systemImage: "tray.and.arrow.up.fill")
}
AccountView()
.badge("!")
.tabItem {
Label("Account", systemImage: "person.crop.circle.fill")
}
}


The key thing here is that by using a view builder, `TabView` avoids needing all the types of the tabs as its type parameter. Whatever view the view builder builds, will be the type of `TabView`&#39;s `Content` type parameter.

We can do something similar, except we don&#39;t know how many tabs there are (See [here][1] for an idea of how SwiftUI can do this), so we will need users to pass an array of &quot;tab identifiers&quot;, and we will then ask the user to give us a tab `View` for each of them.

struct TabbedView<Selection: Hashable, Body: View>: View {

let tabs: [Selection]
@State var selection: Selection

let computeView: (Selection) -&gt; Body

var body: some View {
    VStack {
        // simple view for picking tabs
        Picker(&quot;Tabs&quot;, selection: $selection) {
            ForEach(tabs, id: \.self) { tab in
                // replace this with your custom tab item view
                Text(String(describing: tab))
                // also consider removing the Selection type parameter 
                // and using a concrete type from which you can make
                // a tab item view
            }
        }.pickerStyle(.segmented)

        computeView(selection)
    }
}

init(tabs: [Selection], @ViewBuilder computeView: @escaping (Selection) -&gt; Body) {
    self.tabs = tabs
    self._selection = State(initialValue: tabs.first!)
    self.computeView = computeView
}

}


Usage:

enum Tabs: Int, CaseIterable {
case tab1, tab2, tab3
}

TabbedView(tabs: Tabs.allCases) { selection in
switch selection {
case .tab1:
Text("Tab 1")
case .tab2:
Rectangle().foregroundColor(.blue)
case .tab3:
Button("Button") { }
}
}



  [1]: https://www.wwdcnotes.com/notes/wwdc21/10022/
  [2]: https://developer.apple.com/documentation/swiftui/tabview

</details>



huangapple
  • 本文由 发表于 2023年7月6日 16:04:25
  • 转载请务必保留本文链接:https://go.coder-hub.com/76626725.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定