【问题标题】:Using @ViewBuilder to create Views which support multiple children使用@ViewBuilder 创建支持多个孩子的视图
【发布时间】:2019-10-25 04:57:54
【问题描述】:

SwiftUI 中的一些视图,如 VStack 和 HStack 支持将多个视图作为子视图,如下所示:

VStack {
  Text("hello")
  Text("world")
}

据我所知,他们使用ViewBuilder 使这成为可能,正如here 所解释的那样。

我们如何使用@ViewBuilder 来创建我们自己的支持多个孩子的视图?例如,假设我想创建一个接受任意子级的Layout 视图——如下所示:

struct Layout : View {
  let content: Some View 

  var body : some View {
    VStack {
      Text("This is a layout")
      content()
    }
  } 
}

知道如何在 SwiftUI 中实现这种模式吗?

【问题讨论】:

    标签: swift swiftui


    【解决方案1】:

    这是一个什么都不做的示例视图,只是为了演示如何使用@ViewBuilder

    struct Passthrough<Content>: View where Content: View {
    
        let content: () -> Content
    
        init(@ViewBuilder content: @escaping () -> Content) {
            self.content = content
        }
    
        var body: some View {
            content()
        }
    
    }
    

    用法:

    Passthrough {
        Text("one")
        Text("two")
        Text("three")
    }
    

    【讨论】:

    • 这是完美的。我不太了解View where Content: View@escaping 函数包装器……但这是一个美妙的开始。
    • @bento View 协议有一个关联类型,称为Body,因此你不能说let content: () -&gt; View - 试试吧,你会得到一个编译器错误。 @escaping 意味着闭包将比它所在的上下文(构造函数)寿命更长,并且确实如此,就像在 body 中调用的那样。
    • 很好的解释。但我有一个问题。是否可以像 TabView 那样只显示一个子视图?以您的示例为例,您有 3 个文本,但我一次只想显示其中一个。这意味着我需要在 ViewBuilder 中获取内容的子项。有可能吗?
    • @NikzJon 你可以dump 你的Passthrough 实例来获取它的类型,例如dump(type(of: view)),会打印出类似:Passthrough&lt;SwiftUI.TupleView&lt;(SwiftUI.Text, SwiftUI.Text, SwiftUI.Text)&gt;&gt;。您在TupleView 中看到的泛型数量是通过的视图数量。
    • @NikzJon 假设x 是您的Passthrough 实例,您可以尝试Mirror(reflecting: x.content().value).children.count - 对于上面的示例,它应该返回3
    【解决方案2】:

    使用VStack 的声明,我们需要使用@ViewBuilder 作为我们的内容参数。它是一个闭包,但它不应该是@escaping 如果我们只需要来自闭包的数据,那么存储闭包是不好的。我从 Apple 声明中假设。

    我还认为@inlinable 很重要,因为:

    @inlinable 属性将函数体导出为 模块的接口,使其对优化器可用 从其他模块引用。 More info here

    struct Layout <Content> : View where Content : View {
    
            var content: Content
    
            @inlinable public init(@ViewBuilder content: () -> Content) {
                self.content = content()
            }
    
            var body : some View {
                VStack {
                    Text("This is a layout")
                    self.content
                }
            } 
        }
    

    使用它:

    Layout {           
                Text("1")
                VStack {
                    Text("1")
                    Text("2")
                }
            }
    

    统一更新: 正如Matteo Pacini 指出的有关@escaping 的误导性信息。

    对于DynamicViewContent 视图,我们需要使用@escaping@escaping 用于接受集合(数组、范围等)的视图结构的 Apple 视图结构。因为ForEach 实现了DynamicViewContent - 一种从底层数据集合生成视图的视图。 List 在其初始值设定项中也 ForEach 在内容中

     public init<Data, RowContent>(_ data: Data, selection: Binding<Selection>?, action: @escaping (Data.Element.IdentifiedValue) -> Void, rowContent: @escaping (Data.Element.IdentifiedValue) -> RowContent) where Content == ForEach<Data, Button<HStack<RowContent>>>, Data : RandomAccessCollection, RowContent : View, Data.Element : Identifiable
    

    【讨论】:

    • Re @escaping,Apple 在 List 中使用它,例如在初始化器init(_:action:rowContent:) - 另一个考虑是您正在创建视图结构before body 被调用。当然,这取决于用例,但说不能使用它可能会产生误导。
    • 他们在列表中使用@escaping 为开发人员提供了为每个迭代步骤创建View 的能力。在这种情况下,我们没有迭代,也没有必要在自定义视图中存储闭包。所以是的,你是对的,我们绝对可能需要@escaping 来获取将接收集合的视图
    猜你喜欢
    • 2020-06-09
    • 2021-12-04
    • 2021-10-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-04-02
    • 1970-01-01
    相关资源
    最近更新 更多