DEV Community

Cover image for Building an Elegant Stacked Notification Banner in SwiftUI
Bez
Bez

Posted on Originally published at Medium

Building an Elegant Stacked Notification Banner in SwiftUI

In modern app design, managing screen real estate is critical. In our apps, Blark: a beautifully designed PRO monochrome camera app for iPhone and iPad, and Plomer: a website & domain monitoring app for macOS, iPadOS, and iOS, we needed a way to communicate multiple alerts, errors, or updates to the user; standard pop-ups felt intrusive, and simple lists took up too much space.

Our solution? A stacked notification banner.

By layering messages on top of one another and letting users dismiss them one by one, you create a tactile, visually engaging experience that keeps your UI clean. In this article, we'll walk through how to build a dynamic, animated, stacked banner view using SwiftUI.

The Concept
Our goal is to build a BannerView that:

  • Takes an array of notification messages.
  • Displays the most recent (or most urgent) message prominently at the front.
  • Shows older messages as a "stack" behind the active message.
  • Allows the user to dismiss the top message, smoothly animating the next message into the active spot.
  • Changes styling dynamically based on the severity of the alert (e.g., standard vs. critical issues).

Step 1: Managing the Data
While the focus is on the UI, our view relies on an underlying data model, which we'll call BannerMessageAction. It holds the message string, a subtitle/body, an issue type (to dictate color coding), and a tap action. Our view will take a @Binding array of these models so we can mutate the stack (remove items) directly from the UI.

Step 2: The ZStack Foundation
To create a stacked effect, we naturally reach for a ZStack. The logic is split into two parts:

  • The Background Stack: A limited number of older messages resting behind the main card.
  • The Active Card: The top-most message that the user can interact with.

Rendering the Background Cards
We don't want to render 50 overlapping cards if the user has a lot of notifications. Instead, we limit the visual depth using a maxLength property (defaulting to 2).

ForEach(Array(messages.dropLast().prefix(maxLength).enumerated()), id: \.element.id) { index, message in 
    // Card UI here...
}
Enter fullscreen mode Exit fullscreen mode

By dropping the last element (which represents our active top card) and prefixing the rest to our maxLength, we ensure only a few cards are rendered in the background.

Step 3: The Magic of Offsets and Padding
The true "stacked" 3D effect comes from manipulating padding and Y-offsets based on the card's index in the array.

.padding(.horizontal, CGFloat(maxLength - index) * 8)
.offset(y: CGFloat(maxLength - index) * -8)
Enter fullscreen mode Exit fullscreen mode
  • Horizontal Padding: The further back a card is in the stack (calculated via maxLength - index), the more horizontal padding it receives, making it look narrower than the card in front of it.

  • Vertical Offset: We push the older cards upwards (-8 points per index step) so they peek out from behind the top card.

Step 4: Adding Life with Transitions
A static stack is fine, but SwiftUI makes it incredibly easy to add delightful interactions. When a user taps the "multiply" (close) button, we remove the last item from the array wrapped in a spring animation:

func removeFromStack(){
    withAnimation(.spring(response: 0.4, dampingFraction: 0.8)){
        messages.removeLast()
    }
}
Enter fullscreen mode Exit fullscreen mode

To make the entrance and exit of these cards feel organic, we apply a .asymmetric transition to the cards.

.transition(.asymmetric(
    insertion: .opacity.combined(with: .move(edge: .bottom)),
    removal: .opacity.combined(with: .scale(scale: 0.9))
))
Enter fullscreen mode Exit fullscreen mode

When a notification arrives, it slides up from the bottom while fading in. When you dismiss it, it scales down slightly and fades out, letting the card behind it snap into focus.

The Complete Implementation
Here is the complete code for the BannerView. You'll notice conditional styling based on message.issueType, allowing the banner to swap between an attention-grabbing red for serious issues/warning and a softer yellow for standard updates.

Works well on macOS too

Conclusion
By combining ZStack, dynamic offsets, and asymmetric transitions, you can create a highly interactive and visually appealing notification system. This approach not only saves screen space but also encourages users to actively engage with your app's alerts rather than ignoring a static list.


Blark: a beautifully designed PRO monochrome camera app for iPhone and iPad

Plomer: a website & domain monitoring app for macOS, iPadOS and iOS

Top comments (0)