在 SwiftUI 中使用协议来提供“某些视图”/泛型?

Ger*_*its 2 generics protocols swift swiftui

我正在尝试让我的头脑在 SwiftUI 中得到一些东西。我想构建一个 SwiftUI 视图,并拥有一些可以ViewProvider称为@State var. 像这样的东西:

protocol ViewProvider {
    associatedtype ViewOne = View
    associatedtype ViewTwo = View
    
    @ViewBuilder var viewOne: ViewOne { get }
    @ViewBuilder var viewTwo: ViewTwo { get }
}    

struct ContentView: View {
    @State private var parent: ViewProvider?
    
    var body: some View {
        VStack {
            HStack {
                Button(action: { parent = Father() }, label: { Text("Father") })
                Button(action: { parent = Mother() }, label: { Text("Mother") })
            }
            
            if let p = parent {
                p.viewOne
                p.viewTwo
            }
        }
    }
}

class Father: ViewProvider {
    @ViewBuilder var viewOne: some View {
        Text("Father One!")
    }
    
    @ViewBuilder var viewTwo: some View {
        Text("Father Two!")
    }
}


class Mother: ViewProvider {
    @ViewBuilder var viewOne: some View {
        Text("Mother One!")
    }
    
    @ViewBuilder var viewTwo: some View {
        Text("Mother Two!")
    }
}
Run Code Online (Sandbox Code Playgroud)

这会产生 2 个不同的编译器错误。

@State private var parent: ViewProvider?
// Protocol 'ViewProvider' can only be used as a generic constraint because it has Self or associated type requirements
Run Code Online (Sandbox Code Playgroud)

p.viewOne
p.viewTwo
// 2x Member 'viewOne' cannot be used on value of protocol type 'ViewProvider'; use a generic constraint instead
Run Code Online (Sandbox Code Playgroud)

我对我做错了什么有一个模糊的想法,但不知道如何解决它:)我应该使用什么语法来让这样的东西工作?

kid*_*d_x 8

假设您使用的是 Swift 5.6 或更低版本,问题是您只能使用具有关联类型的协议来实现一致性,即您不能将它们用作传递的类型。原因是对于不同的构象异构体,它们的相关类型会有所不同。

假设您有以下内容:

protocol P {
    associatedtype T
    var prop: T 
}

class MyClass: P {
    var prop: Int 
}

class MyOtherClass: P { 
    var prop: String
}
Run Code Online (Sandbox Code Playgroud)

下面的结果会是什么?

let arr: [P] = [MyClass(), MyOtherClass()]
let myMappedArr = arr.map { $0.prop }
Run Code Online (Sandbox Code Playgroud)

prop每个构象异构体都有不同的类型。


然而,在 Swift 5.7 中,您实际上可以传递此类协议。在 Swift 的更高版本中,您将必须使用关键字any将这些协议作为类型传递。

请参阅解锁存在性提案以了解更多信息。


最后在这里解决不透明类型:

由于您无法传递具有关联类型的协议,因此您不能拥有类似的东西

@State var myState: ViewProvider 甚至@State var myState: some ViewProvider,因为您的状态变量已分配,并且您无法分配不透明类型的内容。

在 SwiftUI 的 View 中,这是有效的,因为view属性是经过计算的,因此可以推断出类型

// type is inferred to be (something like) Group<Text>
var body: some View {
    Group {
       Text("something")
    }
} 
Run Code Online (Sandbox Code Playgroud)

而在这里,您找不到合适的类型来分配给类型不透明的属性

@State var myState: some ViewProvider
...

// You don't know myState's type, so you can't assign anything to it
myState = ... // error - you won't be able to find a matching type to assign to this property
Run Code Online (Sandbox Code Playgroud)

也就是说,@State private var parent: ViewProvider?代码中的行根本无法在 Swift 5.6 或更低版本中编译,因为在函数或计算中使用时,不允许将 ViewProvider 协议用作除一致性之外的任何类型或不透明返回类型。特性。


对所有的编辑表示抱歉。希望提供一些可能的解决方案:

一种方法是简单地使您的 ContentView 通用于其 ViewProvider 的类型

struct ContentView<ViewProviderType: ViewProvider> {
     @State private var parent: ViewProviderType?
     ... 
}
Run Code Online (Sandbox Code Playgroud)

另一种方法是简单地从协议中删除关联类型并删除您尝试返回的视图类型:

protocol ViewProvider {
     var viewOne: AnyView { get }
     var viewTwo: AnyView { get }
}
Run Code Online (Sandbox Code Playgroud)

如果您使用 Swift 5.7,您可以使用类型约束协议作为属性类型,或者您也可以使用主要关联类型,其中您可以声明 type 的属性ViewProvider<MyView>(尽管这不一定能解决您的问题) )。

即使在 Swift 5.7 世界中,ViewProvider 视图类型的泛型或类型擦除可能是您想要做的事情的最佳选择。