SwiftUI Generic Containers

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 +...

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.

Last updated on Sunday, February 1, 2026
entangledDEV. All rights reserved.
Built with Hugo
Theme Stack designed by Jimmy