什么是SwiftUI中的Content?

41

在文档中,我看到Content在不同的上下文环境中:

/// A modifier that can be applied to a view or other view modifier,
/// producing a different version of the original value.
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol ViewModifier {
    /// The content view type passed to `body()`.
    typealias Content
}

还有这里

/// A view that arranges its children in a vertical line.
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public struct VStack<Content> where Content : View {

我在文档中找不到关于Content的适当解释。在SwiftUI中是否有任何预定义的content用法?


内容(content)只是用来表示(generic type)通用类型的一种方式,对于SwiftUI而言,通常代表任何一种视图(view)。要了解有关Swift泛型(generics)的更多信息,请查看:https://docs.swift.org/swift-book/LanguageGuide/Generics.html - kontiki
3个回答

70
重要的是要了解SwiftUI大量使用泛型类型。在SwiftUI(和Combine)发布之前,我从未见过任何使用泛型如此频繁的Swift代码。几乎所有符合View和ViewModifier的类型在SwiftUI中都是泛型类型。
ViewModifier 首先让我们谈谈ViewModifier。ViewModifier是一个协议。其他类型可以符合ViewModifier,但是没有变量或值可以只具有纯ViewModifier类型。
为了使类型符合ViewModifier,我们定义一个body方法,该方法接受Content(无论它是什么)并返回Body(无论它是什么):
func body(content: Content) -> Body

ViewModifier本质上只是一个方法,它以Content作为输入,并返回Body作为输出。

什么是BodyViewModifier将其定义为带有约束的associatedtype

associatedtype Body : View

这意味着我们可以在ViewModifier中选择具体类型,即Body,只要它符合View协议,我们就可以选择任何类型作为Body
那么Content是什么呢?文档告诉你它是一个typealias,这意味着我们可能不能选择它的内容。但是文档没有告诉你Content是什么别名,所以我们不知道body可以用接收到的Content做什么!
文档没有告诉你的原因是,如果符号以下划线(_)开头,Xcode程序会编程不显示SDK中的公共符号。但是,您可以通过查看SwiftUI的.swiftinterface文件来查看ViewModifier的真实定义,包括隐藏的符号。我在这个答案中解释了如何找到该文件。
查阅该文件,我们找到了ViewModifier的真实定义:
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol ViewModifier {
  static func _makeView(modifier: SwiftUI._GraphValue<Self>, inputs: SwiftUI._ViewInputs, body: @escaping (SwiftUI._Graph, SwiftUI._ViewInputs) -> SwiftUI._ViewOutputs) -> SwiftUI._ViewOutputs
  static func _makeViewList(modifier: SwiftUI._GraphValue<Self>, inputs: SwiftUI._ViewListInputs, body: @escaping (SwiftUI._Graph, SwiftUI._ViewListInputs) -> SwiftUI._ViewListOutputs) -> SwiftUI._ViewListOutputs
  associatedtype Body : SwiftUI.View
  func body(content: Self.Content) -> Self.Body
  typealias Content = SwiftUI._ViewModifier_Content<Self>
}

还有一些扩展ViewModifier的方法,定义了body_makeView_makeViewList的默认值,但我们可以忽略这些。

无论如何,我们可以看到Content_ViewModifier_Content<Self>的别名,它是一个struct,没有定义任何有趣的公共接口,但在扩展中符合View。因此,这告诉我们,当我们编写自己的ViewModifier时,我们的body方法将接收某种类型的View(具体类型由框架定义,我们只需称之为Content),并返回某种类型的View(我们可以选择特定的返回类型)。

下面是一个示例ViewModifier,我们可以将其应用于任何View。它给修改后的视图添加填充并给其设置彩色背景:

struct MyModifier: ViewModifier {
    var color: Color

    func body(content: Content) -> some View {
        return content.padding().background(color)
    }
}

请注意,我们不必命名由body返回的View类型。我们可以使用some View并让Swift推断具体类型。
我们可以像这样使用它:
Text("Hello").modifier(MyModifier(color: .red))

VStack

现在让我们来谈谈VStackVStack类型是一个struct,而不是协议。它是泛型的,这意味着它接受类型参数(就像函数接受函数参数一样)。VStack接受一个单一的类型参数,命名为Content。这意味着VStack定义了一组类型,每个类型都允许用于Content

由于VStackContent参数被限制为符合View,这意味着对于每个符合View的类型,都有一个相应的VStack类型。对于Text(符合View),存在VStack<Text>。对于Image,存在VStack<Image>。对于Color,存在VStack<Color>

但是,我们通常不会拼写出正在使用的VStack的完整类型实例,也不会将Content类型作为原始类型,如TextImage。使用VStack的整个原因是在列中排列多个视图。使用VStack告诉Swift在垂直方向上排列其子视图,而VStackContent类型参数指定子视图的类型。

例如,当您编写以下内容时:

VStack {
    Text("Hello")
    Button(action: {}) {
        Text("Tap Me!")
    }
}

实际上,您正在创建此类型的一个实例:

VStack<TupleView<(Text, Button<Text>)>>

这里的Content类型参数是TupleView<(Text, Button<Text>)>类型,它本身是一个带有自己类型参数T的通用类型TupleView,而T在这里是(Text, Button<Text>)(一个2元组,也称为一对)。因此,类型中的VStack部分告诉SwiftUI垂直排列子视图,而TupleView<(Text, Button<Text>)>部分告诉SwiftUI有两个子视图:一个Text和一个Button<Text>
您可以看到,即使是这个简短的示例也会生成具有多层嵌套通用参数的类型。因此,我们肯定希望让编译器为我们解决这些类型。这就是为什么苹果添加了some View语法到Swift中 - 这样我们就可以让编译器找出确切的类型。

你知道如何提取不同元组的内容吗?例如,苹果是如何使用SwiftUI Picker实现的呢?他们必须以某种方式访问标签的内容,对吧? - krjw
原始问题根本没有使用“语义”一词。此外,也不存在“the”语义,因为在不同的上下文中,“Content”意味着不同的事情。我在问题中给出的两个具体示例中解释了“Content”的含义。 - rob mayoff
@Rivera 如果你需要帮助做那个,请发布一个新问题。我认为这和当前问题不相关。 - rob mayoff
@robmayoff 这不是一个问题,而是一个建议,让你的回答更完整。我可以自己编辑它或者发布另一个答案。 - Rivera
如果您想要发布自己的答案,请发表。请不要编辑我的答案。 - rob mayoff
显示剩余4条评论

14

这也可能会有帮助:

private struct FormItem<Content:View>: View {
    var label: String
    let viewBuilder: () -> Content
    var body: some View {
        VStack(alignment: .leading, spacing: 4) {
            Text(label).font(.headline)
            viewBuilder()
        }
    }
}

然后这样使用:

FormItem(label: "Your Name") {
    TextField("name", text: $bindingToName)
}

由于viewBinderstruct的最后一个属性,因此您可以在FormItem函数调用之后放置其内容。


7
我喜欢 Rob's answer,他回答了我发现这个SO问题时隐含的问题,但我认为我可以扩展Kontiki的评论,为新手程序员介绍Swift或通用性。
这个问题询问了几件事情,具体如下:

什么是SwiftUI中的Content?

出乎意料的是,在SwiftUI中实际上没有名为Content的类、结构体或类型(就我所见)! 问题中的两个示例都证明了这一点。
我的意思是什么? Content是一个泛型,有点像“保存类型的变量”(尽管我觉得这种解释很令人困惑)。

泛型非常酷(它们是 Swift 和 XCode 自动完成知道你将字符串放入数组而不是整数的原因),但在这种情况下,通用的 Content 仅被用于表示符合 View 协议的任意类型(例如,一个 Button 总是一个 Button,而不是 Text)。Content 的名称完全是随意的——苹果公司同样可以称其为 FooMyView,如果您正在编写自定义类型来托管其自己的内容,则可以选择任何名称。

如果您使用的语言更多地依赖类和子类(例如Java或C++或基本上每种其他大型、类型化的语言),那么可以说,相对而言,此泛型用于要求所有“内容”遵循“基类”View(需要澄清的是:View不是SwiftUI中的一个类,它是一个协议并具有不同的行为)。除了对于控件的给定实例(例如特定的VStack)外,Content必须始终是相同的类型。一旦是Button,就永远是Button。这就是为什么有时需要使用AnyView的原因。

这一切都有实际的解释

我怎么知道这一切的?看第二个例子:

/// A view that arranges its children in a vertical line.
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public struct VStack<Content> where Content : View {

这段代码声明了名为VStack的结构体,它是一种泛型类型,因为它对应于类型作者选择称为Content的任意结构体/类/类型。不是任何任意类型,因为where Content : View限制调用者只能使用实现了View协议的类型。
Rob的回答解释了另一个例子,即ViewModifierContent也是某种类型的视图(通过检查其他文档)。
查看VStack的初始化器,您会发现它接受一个返回Content的函数:
@inlinable public init(alignment: HorizontalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: () -> Content)

当您创建一个 VStack(或任何其他带有内容的控件)时,您提供的内容实际上在该函数中 - 从那里,Swift 可以确定编译时 Content 的具体(“真实”)类型:
VStack { // Function starts at `{`
    Text("test")
    Text("test 2")
} // Function ends at `}`

如Rob在上文中所解释的,这个函数中的具体类型实际上是某种TupleView。但是因为VStack是泛型的(并且仅限于实现了View的实现者),它只知道您提供了实现View的某个特定类型,而无论是什么类型,我们都称之为Content。

另外: 一些视图

这也在一定程度上解释了SwiftUI中some View的用法:尽管您可以每次使用时编写完整的TupleView类型,但如果您在VStack末尾添加了一个Button,则该函数的类型将从TupleView<(Text, Text)>变为TupleView<(Text, Text, Button)>,这样就需要在需要更改的地方进行繁琐的更改。因此,更容易说它是some View(即,“这是实现视图并忽略其余所有内容的特定类型”)。而使用some View比使用View更安全

返回some View与仅返回View相比有两个重要的区别:

  1. 我们必须始终返回相同类型的视图。
  2. 尽管我们不知道返回的视图类型是什么,但编译器知道。

另外:闭包中的多个返回值?

如果您再次查看我们的 VStack 示例:
VStack { // Function starts at `{`
    Text("test")
    Text("test 2")
} // Function ends at `}`

您会注意到我们使用的函数似乎有2个返回值(两个Text),使用闭包隐式返回语法。但需要注意的是,这种隐式返回仅适用于只有一个表达式的函数(该特性称为单表达式闭包的隐式返回!)。例如:

func foo(n: Int) -> Int {
    n * 4
}

如何返回两个东西?

了解SwiftUI有多好? 描述如下:

但是在 尾随闭包 中,仍然不可能以声明方式将视图一个接一个地放置。如果您注意上面的 HStack init 方法中,最后一个参数是类型为 ( ) -> Content 的函数,并带有 @ViewBuilder 注释。这是 Swift 5.1 的新功能,称为 Function Builders,它使此语法成为可能。

简而言之,在 VStack 中使用的尾随闭包具有 @ViewBuilder 注释,这是一个函数构建器 (Result Builder),它会隐式构造用于 UI 的 TupleView


我不理解的一件事是,例如在 VStack 中,参数 content: () -> Content 是一个返回 Content 的闭包,但当我们初始化一个 VStack 时,我们并没有返回任何东西。在你的例子中,VStack 的尾随闭包只创建了两个 Text 视图,但并没有返回任何东西。这怎么可能呢? - mst4ash
1
@mfshujaie,如果我理解正确的话,这只是Swift中单表达式闭包的隐式返回Closures)的延续,尽管我不知道那两个Text视图如何被隐式转换为TupleView(我认为这就是正在发生的事情)。虽然我想象这就是你的问题:D - citelao
是的,这正是我的问题,那么这两个“Text”视图是如何转换为单个“TupleView”的? - mst4ash
在这里找到了答案:https://blog.avenuecode.com/how-well-do-you-know-swiftui - mst4ash
1
哦,真的很酷!但是在尾随闭包中,仍然不可能以声明性方式将视图一个接一个地放置。如果您注意上面的HStack init方法,最后一个参数是类型为()-> Content的函数,并带有@ViewBuilder注释。这是Swift 5.1的新功能Function Builders,它使此语法成为可能。我会更新我的答案。 - citelao
显示剩余2条评论

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