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:
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
Where to Implement Nsvaluetransformer for Core Data in Swift
Can't Change Uiimageview Image in Function (Swift)
Why Function Return Nil Firebase Swift
How to Convert [Any] to Nsarray
Spritekiit Swift: Touch a Moving Object
Firebase Datadescription Returns Empty Array
Raw Value of Enumeration, Default Value of a Class/Structure, What's the Different
Binary Operator '==' Cannot Be Applied to Operands of Type 'Uilabel' and 'String'
Differencebetween Convenience Init VS Init in Swift, Explicit Examples Better
Using the Swift If Let with Logical and Operator &&
What Does an Exclamation Mark in a Property in Swift Language
How to Make a Uiview Grow in Uiview.Animate() with an Accurate Corner Radius
Getting Data Out of Completionhandler in Swift in Nsurlconnection
Uitextview Linktextattributes Font Attribute Not Applied to Nsattributedstring
How to Correctly Initialize an Unsafepointer in Swift
Using Reflection to Set Object Properties Without Using Setvalue Forkey
.Sink Is Not Returning the Promise Values from a Future Publisher