Want to make your SwiftUI views more reusable, composable, and clean?
This post explores how to build your own generic containers using Swift’s type system and SwiftUI’s powerful @ViewBuilder + closure patterns.

Whether you’re building custom layouts, DSL-like preview wrappers, or logic-driven composition — understanding generic containers is a key unlock.

💡 SwiftUI Tip: Wrapping Views with Generics and Closures#

I recently hit a situation in SwiftUI that seemed small at first — but led to a mini “aha!” moment about closures, generics, and how SwiftUI composes views.

This is one of those tricks that’s easy to overlook until you need it — but once it clicks, it unlocks a whole new layer of composability.

🚧 The Problem#

I wanted to create a wrapper for SwiftUI previews — one that adds common styling like background color and full-frame layout, so I wouldn’t have to repeat this over and over:

#Preview {
    VStack {
        MyCoolView()
    }
    .frame(maxWidth: .infinity, maxHeight: .infinity)
    .appBackground()
}

Simple enough, right?

So I tried to create a wrapper like this:

struct PreviewWrapperView: View {
    let content: some View // ❌ Error!
    
    var body: some View {
        VStack {
            content
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .appBackground()
    }
}

But Swift gave me the error:

“Property declares an opaque return type, but has no initializer expression…”

🔍 The Fix: Use a Generic + Closure#

Swift doesn’t allow some View as a stored property type — it only works for return values.

The correct pattern looks like this:

struct PreviewWrapperView<Content>: View where Content: View {
    @ViewBuilder let content: () -> Content

    var body: some View {
        VStack {
            content()
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .appBackground()
    }
}

And then you use it like this:

#Preview {
    PreviewWrapperView {
        MyCoolView(param: 123)
    }
}

🤯 But wait — doesn’t MyCoolView(param: 123) need a closure like (Int) -> some View?#

This was the part that clicked for me.

No.
You’re not passing MyCoolView as a function.
You’re calling it inside the closure, and returning the result.

That means you’re passing a closure like:

() -> MyView

Which matches () -> Content just fine.

🧠 What this unlocked for me#

I realized this pattern is everywhere in SwiftUI:

  • NavigationStack { ... }
  • Section(header: Text("...")) { ... }
  • Button { ... } label: { ... }

Once you know how to create your own view wrapper using a generic and a closure, you can build anything from:

  • DSL-style layout containers
  • Reusable environment-aware wrappers
  • Onboarding and modal flows
  • Themed previews
  • Stylized layout shells with safe areas, animations, shadows…

🧪 Bonus: Custom Layout Containers#

Now that you understand how to use closures and generics to wrap views, here’s a real-world way to apply this pattern for styling and layout reuse.

Let’s say you want a layout that wraps your screen content in consistent padding, background, and spacing — and optionally adds a footer.

At first, you might be tempted to make the footer closure optional, or try to hack it using AnyView or Footer? tricks. But there’s a cleaner, more Swifty way — and it’s the way Apple does it too.

Instead of creating multiple types, Apple defines multiple initializers in the same struct — and we can do the same.

Here is the wrapper using all we have learned today!

struct StyledScreen<Content: View, Footer: View>: View {
    let content: Content
    let footer: Footer?

    init(
        @ViewBuilder content: () -> Content
    ) where Footer == EmptyView {
        self.content = content()
        self.footer = nil
    }

    init(
        @ViewBuilder content: () -> Content,
        @ViewBuilder footer: () -> Footer
    ) {
        self.content = content()
        self.footer = footer()
    }

    var body: some View {
        VStack(spacing: 24) {
            content
                .padding()
                .background(Color.secondary.opacity(0.1))
                .cornerRadius(12)

            if let footer = footer {
                footer
                    .padding(.top, 32)
            }
        }
        .padding()
        .overlay(
            RoundedRectangle(cornerRadius: 16)
                .stroke(.gray.opacity(0.3), style: StrokeStyle(lineWidth: 1, dash: [6]))
        )
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color(.systemBackground))
    }
}

📦 Usage#

#Preview {
    VStack {
        // With no footer
        StyledScreen {
            Text("No footer")
        }
        
        // With footer
        StyledScreen {
            Text("Has a footer")
        } footer: {
            Button("Continue") { }
        }
    }
}

This version matches Apple’s API design philosophy.

Here is the result:

Result custom wrapper

This line:

init(@ViewBuilder content: () -> Content) where Footer == EmptyView

might look strange at first — but it’s key to enabling a clean, SwiftUI-style API.

💡 Why it exists:#

Our StyledScreen struct uses two generics:

struct StyledScreen<Content: View, Footer: View>

That means Swift needs to know both types — even if the user doesn’t explicitly pass a footer.

If we don’t specify what Footer is, Swift will throw an error like:

“Cannot infer generic parameter ‘Footer’ without more context”

To fix that, we say:

“If the user only provides content, then assume Footer == EmptyView.”

This satisfies the compiler and lets us write:

StyledScreen {
    Text("Hello")
}

Without needing to manually pass an EmptyView.

This pattern is used inside SwiftUI itself — like in NavigationLink, Button, and Section — to support clean, overloaded initializers while keeping everything fully type-safe.

📦 TL;DR#

  • Use Content: View + () -> Content to pass view content into your wrapper
  • Use @ViewBuilder if you want to support multiple children
  • Don’t try to store some View — use a closure instead
  • This unlocks powerful layout composition in SwiftUI

✨ Hope this helped someone else!
If you’ve used this pattern in an interesting way, or have a cool layout abstraction — let me know! I’d love to see what you’re building.