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

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 {
        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)

What We Have So Far

The Banner 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 {
        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
    }
}

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 {
        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

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