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

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.


A pattern I've followed for container views is to use conditional extension conformance to support initializers for the different variations.

Here's an example of a simple Panel view with an optional Footer.

struct Panel<Content: View, Footer: View>: View {        let content: Content    let footer: Footer?        init(@ViewBuilder content: () -> Content, footer: (() -> Footer)? = nil) {        self.content = content()        self.footer = footer?()    }        var body: some View {        VStack(spacing: 0) {            content            // Conditionally check if footer has a value, if desirable.            footer        }    }}// Support optional footerextension Panel where Footer == EmptyView {    init(@ViewBuilder content: () -> Content) {        self.content = content()        self.footer = nil    }}

I believe this is similar to what Apple does to support all the variations of the built-in types. For example, here's a snippet of the headers for a Button.

@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)extension Button where Label == PrimitiveButtonStyleConfiguration.Label {    /// Creates an instance representing the configuration of a    /// `PrimitiveButtonStyle`.    @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)    public init(_ configuration: PrimitiveButtonStyleConfiguration)}@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)extension Button where Label == Text {    /// Creates an instance with a `Text` label generated from a localized title    /// string.    ///    /// - Parameters:    ///     - titleKey: The key for the localized title of `self`, describing    ///       its purpose.    ///     - action: The action to perform when `self` is triggered.    public init(_ titleKey: LocalizedStringKey, action: @escaping () -> Void)    /// Creates an instance with a `Text` label generated from a title string.    ///    /// - Parameters:    ///     - title: The title of `self`, describing its purpose.    ///     - action: The action to perform when `self` is triggered.    public init<S>(_ title: S, action: @escaping () -> Void) where S : StringProtocol}


I would suggest using a ViewModifyer instead of custom Views. Those work like the follwing:

struct SideContent<SideContent: View>: ViewModifier {    var title: String    var sideContent: (() -> SideContent)?    init(title: String) {         self.title = title    }    init(title: String, @ViewBuilder sideContent: @escaping () -> SideContent) {         self.title = title         self.sideContent = sideContent    }    func body(content: Content) -> some View {        HStack {          VStack {             Text(title)             content           }           sideContent?()        }    }}

This may be used as SomeView().modifier(SideContent(title: "asdasd") { Text("asdasd")}), however, if you omit the side, you still need to specify its type SomeView().modifier(SideContent<EmptyView>(title: "asdasd"))

UPDATE

Removing the title it simplifies, as you mentioned.

struct SideContent<SideContent: View>: ViewModifier {    var sideContent: (() -> SideContent)    init(@ViewBuilder sideContent: @escaping () -> SideContent) {        self.sideContent = sideContent    }    func body(content: Content) -> some View {        HStack {            content            sideContent()        }    }}

Also, you can make a modifier for Title.

struct Titled: ViewModifier {    var title: String    func body(content: Content) -> some View {        VStack {            Text(title)            content        }    }}
SomeView()   .modifier(Titled(title: "Title"))   .modifier(SideContent { Text("Side") })