ios 有没有办法在SwiftUI中使用@ViewBuilder创建/提取视图数组

xmq68pz9  于 2023-01-06  发布在  iOS
关注(0)|答案(3)|浏览(147)

我试图创建一个简单的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,但任何能让它看起来更整洁的东西都会有很大帮助)*
pkwftd7m

pkwftd7m1#

很少需要从数组中提取视图。如果您只是希望将@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)
    }
}

如果这对您的用例来说还不够,那么请参见下面的内容。

推荐:我使用我的ViewExtractor包,因为它比这里的代码健壮得多。

我已经有了一个泛型版本,所以不需要为不同长度的元组创建多个初始化器。另外,视图可以是任何你想要的(你不需要限制每个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)
    }
}

结果:

zdwk9cvp

zdwk9cvp2#

任何能让它看起来更整洁的东西都会有很大帮助
好吧,这会让你更接近:
将此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")
)
gv8xihay

gv8xihay3#

我自己在试图找出类似的问题时偶然发现了这一点。所以为了后代和其他人的利益,他们对类似的问题绞尽脑汁。
我发现获取子偏移量的最好方法是使用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)
        }
    }
}

缺点是每个额外的视图都需要一个自定义初始化器,但听起来像是same approach that Apple is using for their stacks
我怀疑基于Mirror的一些东西和说服Swift动态调用可变数量的不同名称的方法可能会起作用。但这很可怕,大约一周前我用完了时间;如果有人有更优雅的东西,我会很感兴趣的。

相关问题