34

Some Views in SwiftUI, like VStack and HStack support having multiple views as children, like this:

VStack {
  Text("hello")
  Text("world")
}

From what I gather, they use ViewBuilder to make this possible as explained here.

How can we use @ViewBuilder for creating our own Views which support multiple children? For example, let's say that I want to create a Layout View which accepts arbitrary children -- something like this:

struct Layout : View {
  let content: Some View 

  var body : some View {
    VStack {
      Text("This is a layout")
      content()
    }
  } 
}

Any idea how to implement this pattern in SwiftUI?

bento
  • 4,846
  • 8
  • 41
  • 59

2 Answers2

73

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")
}
Matteo Pacini
  • 21,796
  • 7
  • 67
  • 74
  • 1
    This is perfect. I don't really understand the `View where Content: View` or `@escaping` function wrapper... but this is a wonderful start. – bento Jun 10 '19 at 20:50
  • 2
    @bento `View` protocol has an associated type, called `Body`, hence you can't say `let content: () -> View` - try it, you will get a compiler error. `@escaping` means that the closure will outlive the context (the constructor) it is in, and it does, as it is called in `body`. – Matteo Pacini Jun 10 '19 at 20:52
  • 1
    Someone already answered while I was composing my answer. @escaping means that it may be performed later when the object of your custom View doesn't exist. It may cause memory leaks. So I've saved the Content as a result of the closure performed. – DenFav Jun 10 '19 at 20:53
  • Here https://github.com/zhuowei/marina/blob/master/marina.swift I found the proof that we should use var content: Content, not a var content: () -> Content. That is why Apple method don't have @escaping keyword. – DenFav Jun 10 '19 at 20:56
  • @DenFav `@escaping` != `memory leaks` - In my case, I've used @escaping to provide lazy initialisation for the view body. In this example, there's no need to instantiate the children in the initialiser. – Matteo Pacini Jun 10 '19 at 21:04
  • 3
    Good explanation. But I have a question. Is it possible to only show one of the subview like what TabView did? Take your example, you have 3 Text, but I only want to show only one of them at a time. That means I need to get the children of content in ViewBuilder. Is it possible? – Bagusflyer Sep 09 '19 at 02:49
  • 1
    @MatteoPacini How can we allow multiple types instead of Text only? – Malav Soni Jun 02 '20 at 05:52
  • 1
    I need 3 views like Button Text Button in VStack – Malav Soni Jun 02 '20 at 05:53
  • I have a question. Is there a way to get the total number of views passed in to `Passthrough` view? – NikzJon Jun 17 '20 at 12:40
  • 1
    @NikzJon you could `dump` your `Passthrough` instance to get its type, e.g. `dump(type(of: view))`, and that would print something like: `Passthrough>`. The number of generics you see inside `TupleView` are the number of views passed. – Matteo Pacini Jun 17 '20 at 16:02
  • That worked. But still I need view count as a variable. i cant figure that out – NikzJon Jun 17 '20 at 17:39
  • 1
    @NikzJon Assuming `x` is your `Passthrough` instance, you could try `Mirror(reflecting: x.content().value).children.count` - it should return `3` for the example above. – Matteo Pacini Jun 17 '20 at 18:49
  • @MatteoPacini this worked outside `Passthrough` definition. Like inside `ContentView()`, it worked fine. But I need the count of view inside init of `Passthrough` without messing with content view. This count variable help me with custom styling of views passed in to the `Passthrough`. – NikzJon Jun 18 '20 at 13:54
8

Using the declaration of VStack we need to use @ViewBuilder for our content parameter. It is a closure but it shouldn't be @escaping it won't be good to store closure if we need only data from it. I assume that from the Apple declarations.

Also I believe that @inlinable is important because:

The @inlinable attribute exports the body of a function as part of a module's interface, making it available to the optimizer when referenced from other modules. More info here

struct Layout <Content> : View where Content : View {
   
    var content: Content
    
    @inlinable public init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }
    
    var body : some View {
        VStack {
            Text("This is a layout")
            self.content
        }
    } 
}

To use it:

Layout {           
    Text("1")
    VStack {
        Text("1")
        Text("2")
    }
}

Update: As Matteo Pacini noted as a misleading info about @escaping.

We need to use @escaping for DynamicViewContent views. @escaping is used Apple's View structures for view structs that are accepting Collections(Array, Range, etc). Because the ForEach implements DynamicViewContent - a type of view that generates views from an underlying collection of data. List in its initializers also ForEach in Content

public init<Data, RowContent>(_ data: Data, selection: Binding<Selection>?, action: @escaping (Data.Element.IdentifiedValue) -> Void, rowContent: @escaping (Data.Element.IdentifiedValue) -> RowContent) where Content == ForEach<Data, Button<HStack<RowContent>>>, Data : RandomAccessCollection, RowContent : View, Data.Element : Identifiable
pkamb
  • 33,281
  • 23
  • 160
  • 191
DenFav
  • 2,683
  • 1
  • 18
  • 27
  • 3
    Re `@escaping`, Apple uses it in List, e.g. in initializer `init(_:action:rowContent:)` - another consideration is that you're creating the view structs `before` body is called. It depends on the use case, of course, but saying it must not be used could be misleading. – Matteo Pacini Jun 10 '19 at 20:56
  • They are using `@escaping` in the List to provide developers an ability to create a `View` for each step of iteration. In this case we have no iterations and it is not necessary for us to store a closure in custom view. So yes, you are correct, we definitely may need `@escaping` for the Views that will receive Collections – DenFav Jun 10 '19 at 21:06