How to Create/Extract an Array of Views Using @Viewbuilder in Swiftui

Is there any way to create/extract an array of Views using @ViewBuilder in SwiftUI

It's rare that you need to extract views from an array. If you are just looking to pass @ViewBuilder content into a view, you can simply do the following:

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)
}
}

If this isn't sufficient for your use-case, then see below.


I have got a generic version working, so there is no need to make multiple initializers for different lengths of tuples. In addition, the views can be anything you want (you are not restricted for every View to be the same type).

You can find a Swift Package I made for this at GeorgeElsham/ViewExtractor. That contains more than what's in this answer, because this answer is just a simplified & basic version. Since the code is slightly different to this answer, so read the README.md first for an example.

Back to the answer, example usage:

struct ContentView: View {

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

Definition of your view:

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 (AKA where all the magic happens):

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)
}
}

Result:

Result

SwiftUI: ViewBuilder unable to create a collection of Views based on array

List items need to conform to Identifiable protocol in order for them to be used as collection data source without the identified(by:) argument.

Xcode error message here is misleading as the software is still in beta.

How to restrict content to Text() on custom view/component using ViewBuilder in SwiftUI

I would like to restrict the number of views to just 1 and it has to
be ONLY Text(). Is there a way to do this?

Yes, you don't need generics for this purpose, you need to specify Text explicitly and compiler will not allow anything else, and Text is not a container, so always will be just one.

struct PickerWidget: View {
var action: () -> Void
private let content: () -> Text

init(action: @escaping () -> Void, @ViewBuilder content: @escaping () -> Text) {
...

Using @ViewBuilder to create Views which support multiple children

Here's an example view that does nothing, just to demonstrate how to use @ViewBuilder.

struct Passthrough<Content>: View where Content: View {

let content: () -> Content

init(@ViewBuilder content: @escaping () -> Content) {
self.content = content
}

var body: some View {
content()
}

}

Usage:

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

Declare a temporary variable or constant inside a closure that returns some View (SwiftUI)

This is due to a limitation where the Swift compiler only tries to infer a closure's return type if it is a single expression. Closure's that are processed by a result builder, such as @ViewBuilder, are not subject to this limitation. Importantly, this limitation also doesn't affect functions (only closures).

I was able to make this work by moving the closure to a method inside the structure. Note: this is the same as @cluelessCoder's second solution, just excluding the @ViewBuilder attribute.

struct GameView: View {
@State private var cards = [
Card(value: 100),
Card(value: 20),
Card(value: 80),
]

var body: some View {
MyListView(items: cards, content: cardView)
}

func cardView(for card: Card) -> some View {
let label = label(for: card) // only called once, and can be reused.
return Text(label)
}

func label(for card: Card) -> String {
return "Card with value \(card.value)"
}
}

Thanks to @cluelessCoder. I would have never stumbled upon this discovery without their input and helpful answer.



Related Topics



Leave a reply



Submit