Using @Viewbuilder to Create Views Which Support Multiple Children

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

SwiftUI Pass two child views to View

Here is possible variant. Tested with Xcode 11.4 / iOS 13.4

struct FlippableView<V1: View, V2: View>: View {
@State private var flipped = false
@State private var degrees = 0.0

var frontCard: V1
var backCard: V2

@inlinable public init(@ViewBuilder content: () -> TupleView<(V1, V2)>) {
let t = content()
self.frontCard = t.value.0
self.backCard = t.value.1
}

var body: some View {
return Group() {
if self.degrees < 90 {
self.frontCard
} else {
self.backCard
.rotation3DEffect(Angle(degrees: 180), axis: (x: CGFloat(0), y: CGFloat(10), z: CGFloat(0)))

}
}
}
}

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

Using protocol to capture SwiftUI view

You need to change the protocol to something like:

protocol RowView {
associatedtype LView: View
associatedtype RView: View

var leftSide: LView { get }
var rightSide: RView { get }
}

Also, use the concrete Example type in the content view instead of the protocol (the protocol you defined doesn't have id at all):

let rows: [Example]

Also! you can make the RowView to be identifiable as your need, So no need for id: \.id anymore:

protocol RowView: Identifiable


A working code:

protocol RowView: Identifiable {
associatedtype LView: View
associatedtype RView: View

var leftSide: LView { get }
var rightSide: RView { get }
}

struct Example: RowView {
var id: Int
var leftSide: some View { Text("Left") }
var rightSide: some View { Text("Right") }
}

struct ContentView: View {
let rows: [Example] = [
.init(id: 1),
.init(id: 2)
]

var body: some View {
VStack {
ForEach(rows) { row in
HStack {
row.leftSide
row.rightSide
}
}
}
}
}

How do you create a SwiftUI view that takes an optional secondary View argument?

November 2021 update (Works in Xcode 11.x, 12.x, and 13.x)

After some thought and a bit of trial and error, I figured it out. It seems a bit obvious in hindsight.

struct SomeCustomView<Content>: View where Content: View {
let title: String
let content: Content

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

// returns a new View that includes the View defined in 'body'
func sideContent<SideContent: View>(@ViewBuilder side: @escaping () -> SideContent) -> some View {
HStack {
self // self is SomeCustomView
side()
}
}

var body: some View {
VStack {
Text(title)
content
}
}
}

It works with or without the method call.

SomeCustomView(title: "string argument") {
// some view
}

SomeCustomView(title: "hello") {
// some view
}.sideContent {
// another view
}

Previous code with subtle bug: body should be self

    func sideContent<SideContent: View>(@ViewBuilder side: @escaping () -> SideContent) -> some View {
HStack {
body // <--- subtle bug, updates to the main View are not propagated
side()
}
}

Thank you Jordan Smith for pointing this out a long time ago.

How does @ViewBuilder work in SwiftUI compared to VStack

so what exactly is the @ViewBuilder doing here?

TL/DR: VStack is just one concrete use of the view-combining behavior that's provided by ViewBuilder.

Many views can take a closure that combines a number of views and returns one view that combines them all. VStack, HStack, and ZStack all do it, of course, but so do Button, Canvas, Form, and Group, Label, List, Menu, and many others. They all use ViewBuilder to combine their views. @ViewBuilder is the reason that they're all limited to 10 subviews, and also the reason you can conditionally include views using if in the list. Ray Wenderlich's tutorial on ViewBuilder is a good place to start to learn how to use it.

So, ViewBuilder's role is to provide the mechanism for combining views, and VStack, HStack, etc. decide how to combine the views.

How exactly is it "combining" all these views?

The short, unsatisfying answer is that ViewBuilder uses @resultBuilder to combine the views you provide. resultBuilder is an attribute that helps you combine things, and ViewBuilder is only one of many uses of it in SwiftUI; there's also SceneBuilder, CommandBuilder, and many others. Explaining result builders is probably a bigger topic than I'd want to cover in a SO answer, but there's plenty of good info out there; look at the Swift docs on result builders for definitive information, and John Sundell's blog post on the subject for a good example.



Related Topics



Leave a reply



Submit