Emptiness in SwiftUI
How "Empty" can be designed and accounted for using EmptyView, EmptyModifier, and ContentUnavailableView
Did you know that zero had to be “invented”? For many ancient civilizations, numbers were exclusively for tangible things. There was no need to count nothing. Eventually, scholars recognized that “nothing” isn’t just an absence—it’s a value. It’s a placeholder, a starting point, and a critical bridge between positive and negative.
In SwiftUI, the empty state is our zero.
There will always be scenarios where there is no data to display, no modifier to apply, or no content to render. Yet, the software must still communicate this state of “nothingness” to the user—or to the compiler.
SwiftUI provides us with three distinct tools to handle emptiness, each serving a different purpose:
EmptyView: For the view hierarchy (Layout).EmptyModifier: For the type system (Compiler).ContentUnavailableView: For the user (Experience).
Let’s explore how to design for nothingness.
EmptyView: The Layout of Nothing
EmptyView is one of SwiftUI’s most deceptively simple types. On the surface, it feels like a placeholder you drop in when you don’t want to render anything. But its role is deeper: EmptyView is SwiftUI’s canonical representation of nothingness.
What EmptyView Actually Is
EmptyView is a zero-sized, non-rendering view. It draws nothing, takes up no space, has no layout implications, and participates minimally in the view tree. If SwiftUI were a standard programming language, EmptyView would be Void—a way to say, “There is a concept of a view here, but it is empty.”
Under the hood, SwiftUI uses EmptyView as the default return type for conditional branches that don’t produce UI and for erased view builders.
Common Use Cases
1. Handling Optional Content
if let user = model.user {
UserProfileView(user: user)
} else {
EmptyView()
}
This is the simplest case: you conditionally include a view, and use EmptyView() when there’s nothing to show.
2. Satisfying ViewBuilders
SwiftUI’s @ViewBuilder cannot return nil. It must return a View. In complex generic contexts or manual ViewBuilder implementations, EmptyView is the correct return type when you need to bail out of rendering.
How EmptyView Differs From .hidden()
This is where many developers trip up.
Using .hidden() does not remove your view—it’s closer to setting visibility: hidden in CSS. The view stays in the layout calculation, reserves its frame size, and affects the positioning of neighbors.
Text("Hidden")
.hidden() // Invisible, but still takes up space
Using EmptyView removes the view entirely from the layout pass:
EmptyView() // No space, no layout, no impact
Captain’s Tip
EmptyView is the silence between notes—essential to the rhythm, but never the melody. Use it to create space, not confusion.
This article would like to promote Jacob’s Tech Tavern:
Jacob’s blog was one of the few that inspired me to get back into writing articles and on Substack. His topics are always interesting, in-depth, and full of good humor. And now that he’s gone full-time indie, we get to enjoy more of his great work! Check him out and consider subscribing to Jacob’s Tech Tavern, today!
EmptyModifier: The Compiler’s Nothing
If EmptyView represents “no view,” then EmptyModifier represents “no transformation.”
The official documentation notes that this modifier is useful “at compile time.” This is a crucial distinction. EmptyModifier isn’t usually meant for runtime logic (like toggling opacity on and off); it is the Identity element of the modifier world. It exists to satisfy the type checker when a modifier is required, but you don’t actually want to do anything.
The Power of Type Aliases
The most powerful use case for EmptyModifier is combining it with Conditional Compilation.
Imagine you want a specific debug overlay—a red border or a tap gesture—but you want that code completely stripped out of your Release builds. You don’t want to wrap every call site in #if DEBUG.
Instead, you can define a modifier that changes based on the build configuration:
#if DEBUG
struct DebugBorder: ViewModifier {
func body(content: Content) -> some View {
content.overlay(
RoundedRectangle(cornerRadius: 4)
.stroke(.red, lineWidth: 1)
)
}
}
#else
// In Release builds, this becomes the "Identity" modifier
typealias DebugBorder = EmptyModifier
#endif
Now, your call site remains clean and consistent:
Text("Hello World")
.modifier(DebugBorder())
In a Debug build, the compiler sees a ViewModifier that draws a border.
In a Release build, the compiler sees EmptyModifier. Because EmptyModifier’s body just returns the content exactly as is, the compiler can optimize it away entirely. There is no runtime cost, no branching logic, and no leftover code artifacts.
Why not use it for Runtime Logic?
You might be tempted to write code like this:
view.modifier(isEnabled ? MyEffect() : EmptyModifier())
While this compiles, it usually results in the creation of a _ConditionalContent wrapper. For simple runtime toggling, it is often cleaner to put the logic inside a custom modifier or use standard view extensions.
EmptyModifier shines when you need to provide a default value to a generic system. If you are building a reusable component that accepts a generic VM: ViewModifier, you can set the default to EmptyModifier:
struct MyContainer<VM: ViewModifier>: View {
var modifier: VM = EmptyModifier() // Default is "do nothing"
// ...
}
Captain’s Tip
EmptyModifier is the invisible rigging of your UI—unseen in release builds, but essential when you’re tuning the ship. Use it to debug boldly and deploy cleanly.
The Paradox of Empty State
“Empty” is one of the most misleading words in UI design.
In software, an empty state is never truly empty—it’s a moment of uncertainty. It’s the space where users ask: What should I do next? Did something break? Is there supposed to be something here?
Left untreated, emptiness feels like failure. Treated well, it becomes guidance.
While EmptyView solves the technical problem of rendering nothing, it fails the user experience problem. An empty screen requires intention, clarity, and often a visible invitation to act.
ContentUnavailableView: Designing for Absence
This is precisely why SwiftUI introduced ContentUnavailableView.
Rather than treating empty states as an edge case or a blank canvas, ContentUnavailableView formalizes them. It gives emptiness a standard structure that feels “at home” on Apple platforms.
Here, we have a really basic implementation that still manages to not leave the user hanging:
ContentUnavailableView("Emptiness!", systemImage: "bathtub", description: Text("Wow, such empty!"))This next example provides a bit more information that answers three simple questions considering “nothingness”:
What’s missing? (The Title)
Why is it missing? (The Description)
What can I do next? (The Action)
ContentUnavailableView {
Label("No Favorites", systemImage: "star.slash")
} description: {
Text("Mark items as favorites to see them here.")
} actions: {
Button("Browse Items") {
// Navigate to items
}
}
Instead of a blank list or a silent screen, the UI acknowledges the state. It reassures the user that the app isn’t broken—it’s just waiting.
In many cases, ContentUnavailableView is the right answer where developers used to reach for EmptyView. If a list is empty, don’t show an EmptyView (which shows nothing); show a ContentUnavailableView (which explains why there is nothing).
Captain’s Tip
An empty state isn’t a failure of your UI—it’s a moment of guidance. When there’s nothing to show, your job shifts from displaying content to setting expectations. If users feel lost in an empty screen, the problem isn’t the data—it’s the design.
Conclusion
Emptiness in SwiftUI is a spectrum.
At the lowest level, we have EmptyView: the structural, silent void used by the layout system to reserve no space.
At the compiler level, we have EmptyModifier: the identity type used to strip behaviors out of release builds or satisfy generic requirements without incurring runtime cost.
And at the human level, we have ContentUnavailableView: the loud, helpful interface that turns a lack of data into a call to action.
As you build your apps, remember that “nothing” is a valid state that deserves just as much care as your “happy paths.” Use EmptyView to hide, EmptyModifier to optimize, and ContentUnavailableView to guide.






