有啥方法可以在 SwiftUI 中使用 @ViewBuilder 创建/提取视图数组

Posted

技术标签:

【中文标题】有啥方法可以在 SwiftUI 中使用 @ViewBuilder 创建/提取视图数组【英文标题】:Is there any way to create/extract an array of Views using @ViewBuilder in SwiftUI有什么方法可以在 SwiftUI 中使用 @ViewBuilder 创建/提取视图数组 【发布时间】:2020-07-04 14:31:13 【问题描述】:

我正在尝试创建一个简单的struct,它接受一个视图数组并返回一个普通的VStack,其中包含这些视图,除了它们都是对角堆叠的。

代码:

struct LeaningTower<Content: View>: View 
    var views: [Content]
    var body: some View 
        VStack 
            ForEach(0..<views.count)  index in
                self.views[index]
                    .offset(x: CGFloat(index * 30))
            
        
    

现在这很好用,但每当我不得不调用它时,我总是很生气:

LeaningTower(views: [Text("Something 1"), Text("Something 2"), Text("Something 3")])

在这样的数组中列出视图对我来说似乎非常奇怪,所以我想知道是否有一种方法可以使用 @ViewBuilder 来调用我的 LeaningTower 结构,如下所示:

LeaningTower   // Same way how you would create a regular VStack
    Text("Something 1")
    Text("Something 2")
    Text("Something 3")
    // And then have all of these Text's in my 'views' array

如果有办法使用@ViewBuilder 创建/提取视图数组,请告诉我。

(即使实际上无法使用@ViewBuilder,任何让它看起来更整洁的东西都会有很大帮助)

【问题讨论】:

【参考方案1】:

很少需要从数组中提取视图。如果您只是想将@ViewBuilder 内容传递到视图中,您可以简单地执行以下操作:

struct ContentView: View 
    var body: some View 
        VStackReplica 
            Text("1st")
            Text("2nd")
            Text("3rd")
        
    


struct VStackReplica<Content: View>: View 
    @ViewBuilder let content: () -> Content

    var body: some View 
        VStack(content: content)
    

如果这对您的用例来说还不够,请参见下文。


我有一个通用版本,所以不需要为不同长度的元组创建多个初始化器。此外,视图可以是任何你想要的(你不会被限制为每个View 都是相同的类型)。

您可以在 GeorgeElsham/ViewExtractor 找到我为此制作的 Swift 包。这包含的不仅仅是这个答案中的内容,因为这个答案只是一个简化的基本版本。由于代码与此答案略有不同,因此请先阅读README.md 以获取示例。

回到答案,示例用法:

struct ContentView: View 
    
    var body: some View 
        LeaningTower 
            Text("Something 1")
            Text("Something 2")
            Text("Something 3")
            Image(systemName: "circle")
        
    

你的观点的定义:

struct LeaningTower: View 
    private let views: [AnyView]
    
    init<Views>(@ViewBuilder content: @escaping () -> TupleView<Views>) 
        views = content().getViews
    
    
    var body: some View 
        VStack 
            ForEach(views.indices)  index in
                views[index]
                    .offset(x: CGFloat(index * 30))
            
        
    

TupleView 扩展名(也就是所有魔法发生的地方):

extension TupleView 
    var getViews: [AnyView] 
        makeArray(from: value)
    
    
    private struct GenericView 
        let body: Any
        
        var anyView: AnyView? 
            AnyView(_fromValue: body)
        
    
    
    private func makeArray<Tuple>(from tuple: Tuple) -> [AnyView] 
        func convert(child: Mirror.Child) -> AnyView? 
            withUnsafeBytes(of: child.value)  ptr -> AnyView? in
                let binded = ptr.bindMemory(to: GenericView.self)
                return binded.first?.anyView
            
        
        
        let tupleMirror = Mirror(reflecting: tuple)
        return tupleMirror.children.compactMap(convert)
    

结果:

【讨论】:

嗨乔治,这是一个非常有趣的方法。你介意解释一下 makeArray 函数在做什么吗? @lenny 我在TupleView 中创建一个元组值的Mirror。这包含所有视图。因为View 是通用的并且具有关联类型,所以我不能像as? View 那样进行转换。相反,我将视图的内存绑定到ViewGenericView 的结构的“重新创建”。然后将其转换为 AnyView。基本上整个函数将一个元组转换为一个数组,没有泛型问题。 嗨@George!有趣的方法!根据我的阅读,应该避免使用AnyView,因为它会删除其内容的类型结构,使得 SwiftUI 无法有效地知道视图树的哪些部分需要重新渲染。你认为这会是一个问题吗?或者可能仅适用于作为content 传入的视图之间存在共享状态的特定用例? @Martin 非常好的问题 - 通常 AnyView 不是什么大问题 - 请参阅 here。一般来说,如果数据变化不大并且没有复杂的动画,你会完全没问题。这里的性能差异可以忽略不计。我不确定的是,这是否只影响***视图,而不影响子视图(我的意思是如果你传入VStack,那么里面的一切都会正常工作吗?)。无论哪种方式,它都不应该真正引起关注,除非您使用它有 1000 行,否则可能会影响性能。 @Martin 如果可能,您应该坚持使用通常内置的HStackVStackForEach 等。您仍然可以将内容传递给它们。此扩展更适用于复杂且罕见的用例。不要在你的应用中到处使用它。【参考方案2】:

在尝试自己找出类似的东西时遇到了这个问题。因此,为了后代和其他人的利益,他们正在努力解决类似的问题。

我发现获得子偏移量的最佳方法是使用 Swift 的键入机制,如下所示。例如

struct LeaningTowerAnyView: View 
    let inputViews: [AnyView]

    init<V0: View, V1: View>(
        @ViewBuilder content: @escaping () -> TupleView<(V0, V1)>
    ) 
        let cv = content().value
        inputViews = [AnyView(cv.0), AnyView(cv.1)]
    

    init<V0: View, V1: View, V2: View>(
        @ViewBuilder content: @escaping () -> TupleView<(V0, V1, V2)>) 
        let cv = content().value
        inputViews = [AnyView(cv.0), AnyView(cv.1), AnyView(cv.2)]
    

    init<V0: View, V1: View, V2: View, V3: View>(
        @ViewBuilder content: @escaping () -> TupleView<(V0, V1, V2, V3)>) 
        let cv = content().value
        inputViews = [AnyView(cv.0), AnyView(cv.1), AnyView(cv.2), AnyView(cv.3)]
    

    var body: some View 
        VStack 
            ForEach(0 ..< inputViews.count)  index in
                self.inputViews[index]
                    .offset(x: CGFloat(index * 30))
            
        
    

用法是

struct ContentView: View 
    var body: some View 
        LeaningTowerAnyView 
            Text("Something 1")
            Text("Something 2")
            Text("Something 3")
        
    

它也适用于任何视图,而不仅仅是文本,例如

struct ContentView: View 
    var body: some View 
        LeaningTowerAnyView 
            Capsule().frame(width: 50, height: 20)
            Text("Something 2").border(Color.green)
            Text("Something 3").blur(radius: 1.5)
        
    

缺点是每个额外的 View 都需要一个自定义初始化器,但听起来像是 same approach that Apple is using for their stacks

我怀疑基于Mirror 并说服 Swift 动态调用可变数量的不同名称的方法可能会起作用。但这变得很可怕,大约一周前我没时间了;-)。如果有人有更优雅的东西,我会很感兴趣。

【讨论】:

如果您的容器中不需要那么多视图,则效果非常好(以及混合视图)。【参考方案3】:

从字面上看,任何能让它看起来更整洁的东西都会有很大帮助

嗯,这会让你更接近:

将此init 添加到您的LeaningTower

init(_ views: Content...) 
    self.views = views

然后像这样使用它:

LeaningTower(
    Text("Something 1"),
    Text("Something 2"),
    Text("Something 3")
)

您可以将偏移量添加为可选参数:

struct LeaningTower<Content: View>: View 
    var views: [Content]
    var offset: CGFloat
    
    init(offset: CGFloat = 30, _ views: Content...) 
        self.views = views
        self.offset = offset
    
    
    var body: some View 
        VStack 
            ForEach(0..<views.count)  index in
                self.views[index]
                    .offset(x: CGFloat(index) * self.offset)
            
        
    

示例用法:

LeaningTower(offset: 20,
    Text("Something 1"),
    Text("Something 2"),
    Text("Something 3")
)

【讨论】:

如果我想同时使用不同的视图类型怎么办?

以上是关于有啥方法可以在 SwiftUI 中使用 @ViewBuilder 创建/提取视图数组的主要内容,如果未能解决你的问题,请参考以下文章

有啥方法可以在 SwiftUI 中使用 @ViewBuilder 创建/提取视图数组

有啥方法可以判断哪个状态更改导致在 SwiftUI 中重建视图?

有啥方法可以避免 SwiftUI GeometryReader 阻止嵌套视图在列表中增长?

有啥方法可以将视图从 swiftUI 更改为 UIKit?

有啥方法可以在 SwiftUI 中的可搜索导航修饰符上关闭自动更正/设置键盘类型

在 UIKit 中我们可以设置 label.numberOfLines = 0 当我们不知道文本将占用多少行时?在 SwiftUI 中有啥替代方法? [复制]