How do you create a SwiftUI view that takes an optional secondary View argument?
Ava Hall
Updated on March 08, 2026
I am trying to create a custom SwiftUI view that acts like the default views where I can add extra content to a view with a method or optional initializer argument.
SomeCustomView(title: "string argument") { // some view } SomeCustomView(title: "hello") { // some view }.sideContent { // another view } // This style is acceptable too SomeCustomView(title: "hello", sideContent: { /* another view */ }) { // some view } How can I modify this view struct to behave like the above example?
struct SomeCustomView<Content>: View where Content: View { let title: String let content: Content init(title: String, @ViewBuilder content: () -> Content) { self.title = title self.content = content() } var body: some View { VStack { Text(title) content } } } Ideally I'd have two different body "templates" that I could switch between depending on if the sideContent method was called or sideContent parameter was set. For example,
var body: some View { VStack { Text(title) content } } // or var otherBody: some View { HStack { VStack { Text(title) content } sideContent } } 33 Answers
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.
4A 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 footer extension 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 } 1I 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") }) 2ncG1vNJzZmirpJawrLvVnqmfpJ%2Bse6S7zGiorp2jqbawutJobHFraGmCeXyOoaawZZSkerq71Gaaq52RqbJurYysrqKepKq2bsLInq5mrJiWwW7AwKScrGWRo3qwvNOipqeZnGLApq%2FOp5uaqqliw6qx1maYq5%2BlorKvwA%3D%3D