Notification Banner using SwiftUI

Getting Started

Begin by creating a new blank project in Xcode. Select the Single View Template, and make sure you set your project to use SwiftUI.

Instead of creating a custom SwiftUI View today, we'll be creating a ViewModifier. A ViewModifier From the Documentation, "A modifier that you apply to a view or another view modifier, producing a different version of the original value." Essentially at the end of this, we're going to be able to add a banner to any view we want throughout our app with a single line.

Start off by creating a new SwiftUI ViewModifier file and name it BannerModifier.swift

Just above the body variable, go ahead and define a new struct called BannerData. Inside define two variables, title and detail. These will be used as the text for your banner.

struct BannerData {
var title:String
var detail:String
}

Now declare an instance of BannerData above your body function. For this variable we're going to make it @Binding. This will allow data of the banner to change over time. Thus being able to change the title and details.

@Binding var data:BannerData

Since this banner needs to display over all other content on screen when displayed, we’ll need to utilize a ZStack. This will allow us to place all of the content from the body function below the views for our banner.

Within the body closure, delete the placeholder that was generated for you and add in a ZStack. Inside place the content and VStack for the banner.

ZStack {
VStack {
// Banner Content Here
Spacer()
}
content
}

The inner VStack and Spacer will allow the banner content to be pushed to the top of the screen.

Inside the VStack, place a second VStack. Inside the inner one, place two Text views. Use the variables declared inside data as input for both Text views. Go ahead and also give the two Text views some styling. Here's what your body should look like.

func body(content: Content) -> some View {
ZStack {
VStack {
// Banner Content Here
VStack(alignment: .leading, spacing: 2) {
Text(data.title)
.bold()
Text(data.detail)
.font(Font.system(size: 15, weight: Font.Weight.light, design: Font.Design.default))
}

Spacer()
}
content
}
}

I’ll quickly add a HStack in order for our banner to take up the full width of the screen when shown.

func body(content: Content) -> some View {
ZStack {
VStack {
HStack {
// Banner Content Here
VStack(alignment: .leading, spacing: 2) {
Text(data.title)
.bold()
Text(data.detail)
.font(Font.system(size: 15, weight: Font.Weight.light, design: Font.Design.default))
}
Spacer()
}

Spacer()
}
content
}
}

Then to make this banner look a little better, we’ll add some styling to the inner VStack as such:

VStack(alignment: .leading, spacing: 2) {
// Text Views From Before...
}
.foregroundColor(Color.white)
.padding(12)
.background(Color(red: 67/255, green: 154/255, blue: 215/255))
.cornerRadius(8)
The Banner we have so far.

Next we’ll add a show variavle which will toggle the display of our banner. When this variable changes value, the banner will hide/display at the top of the device. First define it above the body.

@Binding var show:Bool

Then wrap the outer VStack in the conditional statement.

func body(content: Content) -> some View {
ZStack {
if show {
VStack {
HStack {
VStack(alignment: .leading, spacing: 2) {
Text(data.title)
.bold()
Text(data.detail)
.font(Font.system(size: 15, weight: Font.Weight.light, design: Font.Design.default))
}
Spacer()
}
.foregroundColor(Color.white)
.padding(12)
.background(data.level.tintColor)
.cornerRadius(8)
Spacer()
}
}
content
}
}

WE’re going to define an extension to View that way the BannerModifier can be added to any View, in addition to letting us bind our data and display variables. See below for how I did it.

extension View {
func banner(data: Binding<BannerModifier.BannerData>, show: Binding<Bool>) -> some View {
self.modifier(BannerModifier(data: data, show: show))
}
}

For our banner, we’re going to have four distinct types. Info, Success, Warning, and Error. This will allow us to set four different colors depending on the type of banner being displayed.

To make this easy, we’re going to be using enumerations. Start by defining one named BannerType and create cases for the four I mentioned above.

enum BannerType {
case Info
case Warning
case Success
case Error
}

Now to allow us to get a specific color based on the case, you need to add a variable for color, tintColor.

enum BannerType {
case Info
case Warning
case Success
case Error

var tintColor: Color {
switch self {
case .Info:
return Color(red: 67/255, green: 154/255, blue: 215/255)
case .Success:
return Color.green
case .Warning:
return Color.yellow
case .Error:
return Color.red
}
}
}

To keep track of the BannerType we need to add a instance of it in our BannerData struct.

struct BannerData {
var title:String
var detail:String
var type: BannerType
}

…and to change the banner background color based on the BannerType we change code we defined earlier in our body func.

.background(data.type.tintColor)

Running A Live Example

Let’s put to use what we have so far and show a live example of the banner. Go ahead copy the below code as your ContentView in your main view file.

struct ContentView: View {

@State var showBanner:Bool = true
@State var bannerData: BannerModifier.BannerData = BannerModifier.BannerData(title: "Default Title", detail: "This is the detail text for the action you just did or whatever blah blah blah blah blah", type: .Info)

var body: some View {
Text("Hello Trailing Closure")
.banner(data: $bannerData, show: $showBanner)
}
}

Simply replace the Text view with a button as such:

var body: some View {
Button(action: {
self.showBanner = true
}) {
Text("[ Show ]")
}.banner(data: $bannerData, show: $showBanner)
}

We want the banner to slide down onto screen and then slip away after a certain time or when the user taps the banner. In order to do so we need to add some code to the banner’s VStack. See the updated body func of BannerModifier

func body(content: Content) -> some View {
ZStack {
if show {
VStack {
HStack {
VStack(alignment: .leading, spacing: 2) {
Text(data.title)
.bold()
Text(data.detail)
.font(Font.system(size: 15, weight: Font.Weight.light, design: Font.Design.default))
}
Spacer()
}
.foregroundColor(Color.white)
.padding(12)
.background(data.type.tintColor)
.cornerRadius(8)
Spacer()
}
.padding()
.animation(.easeInOut)
.transition(AnyTransition.move(edge: .top).combined(with: .opacity))
.onTapGesture {
withAnimation {
self.show = false
}
}.onAppear(perform: {
DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
withAnimation {
self.show = false
}
}
})
}
content
}
}

Notice the .animation() and .transition() functions. These slide the view down from the top when it's shown as well as changing the opacity.

Then just below .onTapGesture() and .onAppear() hide the banner hwne the user taps on it or afte a pre determined amount of time.

Running The Final Product

banner_gif
banner_gif

Creator of TrailingClosure.com, and an avid learner!

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store