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.
ViewModifier
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.
BannerModifier
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
Constructing The Body
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.
ZStack
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 {
content
VStack {
// Banner Content Here
Spacer()
}
}
The inner VStack
and Spacer
will allow the banner content to be pushed to the top of the screen.
Banner Layout
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 {
content
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()
}
}
}
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 {
content
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()
}
}
}
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)
What We Have So Far
Toggling Display of The Banner
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 {
content
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()
}
}
}
}
Extending View
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))
}
}
Changing the Banner Color
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)
}
}
Activating Banner With a Button
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)
}
Animating The Banner
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 {
content
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
}
}
})
}
}
}
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
Modifying Banner
Since you're also passing in data to the banner, you can change it's Type
as well as Title
and Detail
on the fly.
Try replacing your main body code with this.
VStack(alignment: .center, spacing: 4) {
Button(action: {
self.bannerData.type = .Info
self.showBanner = true
}) {
Text("[ Info Banner ]")
}
Button(action: {
self.bannerData.type = .Success
self.showBanner = true
}) {
Text("[ Success Banner ]")
}
Button(action: {
self.bannerData.type = .Warning
self.showBanner = true
}) {
Text("[ Warning Banner ]")
}
Button(action: {
self.bannerData.type = .Error
self.showBanner = true
}) {
Text("[ Error Banner ]")
}
}.banner(data: $bannerData, show: $showBanner)
Full Code Available on Github
Checkout the full project on my Github