Swift存在类型:如何创建符合协议的通用视图?

33qvvth1  于 2023-10-15  发布在  Swift
关注(0)|答案(1)|浏览(154)

我正在构建一个SwiftUI macOS应用程序,它可以操作一组视图(“Playground”)。每个Playground都有一些特定的信息,但都共享一个通用的公共视图。我还希望枚举列出我的应用程序中所有可用的视图。
我努力使用Swift存在类型和协议来建模我的数据结构。特别是,我总是以错误“Type any View can not conform to protocol View”结束,尽管我读过许多关于存在类型的文档和博客文章,以及为什么协议不能符合自身。我在这一点上受阻。
下面是我的代码:

// the protocol all my playgrounds must implement
protocol PlaygroundComponent {
    associatedtype Body: View
    @ViewBuilder var component: Self.Body { get }
}

// a generic type that implements the common part of all the playgrounds + the specific part from subtypes
struct PlaygroundView<Content: PlaygroundComponent>: View {
    let content : Content
    var body: some View {
        Text("reused content")
        content.component
    }
}

// a concrete playground, it defines only the specific part 
struct ListModelsPlayground: PlaygroundComponent {
    var component: some View {
        Text("ListModelsPlayground")
    }
}

// an enum that lists all the implementations I have (only one to keep this example short)
enum Playgrounds  {
    case listModels
    var info: any PlaygroundView<PlaygroundComponent> { ListModelsPlayground() } // ---> compilation error : Type 'any PlaygroundComponent' cannot conform to 'PlaygroundComponent'
}

我如何建模一个带有单个子组件的通用视图,以便以后成为这种结构或List( ForEach { } ) SwiftUI视图的一部分?

4xrmg8kj

4xrmg8kj1#

首先,PlaygroundComponent只是重新发明了View。你不需要如果Playground组件有一些您想要建模的公共属性,那就是 data,它们不属于视图。如果每个Playground都有自己独特的属性,那就是@State
作为一个例子,让我们实现两个playgrounds views

struct ListModelsPlayground: View {
    var body: some View {
        Text("ListModelsPlayground")
    }
}

struct AnotherPlayground: View {
    var body: some View {
        Text("Another Playground")
    }
}

现在假设您需要一个枚举,以便将这些playground放入一个数组中,并以List或其他格式显示它们。这就是 data,这就是PlaygroundComponent s的公共属性所在。

enum Playground: Identifiable, CaseIterable {
    case listModels
    case another
    
    var id: Playground { self }
    
    // let's suppose each playground has a title property, as an example
    var title: String {
        switch self {
        case .listModels: return "List Models"
        case .another: return "Another"
        }
    }
    // if you have more common properties,
    // perhaps bundle them all into a struct
}

请注意,如果公共属性可以更改,则应该使用struct,其中static let s表示每种类型的Playground。
现在,我们可以在List中显示数组Playground.allCases,也可以在NavigationSplitView中显示数组Playground.allCases,因此单击其中一个会转到PlaygroundView

@State var selectedPlayground: Playground?

var body: some View {
    NavigationSplitView {
        List(Playground.allCases, selection: $selectedPlayground) { playground in
            Text(playground.title)
        }
    } detail: {
        if let selectedPlayground {
            PlaygroundView(component: selectedPlayground)
        }
    }

}

注意PlaygroundView如何只需要一个playground。这就是它真正需要的-数据。它可以用它来决定显示哪个Playground。

struct PlaygroundView: View {
    let component: Playground // make this a @Binding if the playground view can change the common properties
    var body: some View {
        Text("reused content")
        switch component {
        case .listModels: ListModelsPlayground()
        case .another: AnotherPlayground()
        }
    }
}

如果你真的不喜欢这个switch,你可以将它作为一个属性提取到枚举中:

@ViewBuilder
var view: some View {
    switch self {
    case .listModels: ListModelsPlayground()
    case .another: AnotherPlayground()
    }
}

相关问题