Show a banner from anywhere in SwiftUI using ViewModifier

November 05, 2019

Creating view isn’t hard with SwiftUI, we can quickly iterate to build our final struct BannerView: View. But what if we would like to display it on top of our content? What if we have multiple View that needs to show a banner? Aren’t we going to duplicate code in lot of places with also defining how we want to animate in/out the transition?

The solution

Create a BannerView

It could be anything here, up to you to decide how it looks like, what do we need to show in a banner. Does it needs a Title? A subtitle? A button? For our example we will just need a data structure BannerData to represent what the banner needs to display.

struct BannerData {
    let title: String
    var actionTitle: String? = nil
    // Level to drive tint colors and importance of the banner.
    var level: Level = .info

    enum Level {
        case .warning
        case .success

        var tintColor: Color {
            switch self {
            case .warning: return .yellow
            case .success: return .green
            }
        }
    }
}

struct BannerView: View {
    let data: BannerData
    var action: (() -> Void)?

    var body: some View {
        HStack {
            Text(data.title)
            Spacer()
            Button(action:
                action?()
            ) {
                Text(data.actionTitle ??Action)
                    .foregroundColor(.white)
                    .padding(EdgeInsets(top: 6, leading: 12, bottom: 6, trailing: 12))
                    .background(Rectangle().foregroundColor(Color.black.opacity(0.3)))
            }
        }
        .padding(EdgeInsets(top: 12, leading: 8, bottom: 12, trailing: 8))
        .background(data.level.tintColor)
    }
}

In your case, you might want to use an image, a subtitle, an option to be dismissible or not. But for this example we just decided to create a simple BannerView .

Display the banner the naive way

Having both BannerData and BannerView we could already display it from any other SwiftUI View.

@State private var showBanner: Bool = false
@State private var countTap: Int = 0

var body: some View {
    VStack {
        if showBanner {
            BannerView(data: BannerData(title:Naive way”, actionTitle:OK, level: .warning), action: {
                self.countTap += 1
            })
            .animation(.easeInOut)
            .transition(AnyTransition.move(edge: .top).combined(with: .opacity))
            .onTapGesture {
                self.showBanner = false
            }
        }
        Text(Some text”)
    }
}

I call this the naive way because we are doing too much:

  • We know that a banner will be displayed, so we wrap everything in a VStack.
  • We use have a local @State var showBanner to display it or not.
  • We define the animation and transition.

Nothing is wrong, it could just be improved. The syntax could be more concise and less repetitive accross your app.

Introducing ViewModifier

From Apple documentation, this is what we can read about ViewModifier.

A modifier that can be applied to a view or other view modifier, producing a different version of the original value.

The cool part is the second part of the sentence. We can produce a different version of the original value? Isn’t this what we want with a banner?

Given a @State showBanner we need to add a VStack { } to display the BannerView on top of the rest of the content.

So I gave it a try and this is how my ViewModifier looks like for this banner example:

struct BannerViewModifier: ViewModifier {
    @Binding var isPresented: Bool
    let data: BannerData
    let action: (() -> Void)?

    func body(content: Content) -> some View {
        VStack(spacing: 0) {
            if isPresented {
                BannerView(data: data)
                    .animation(.easeInOut)
                    .transition(AnyTransition.move(edge: .top).combined(with: .opacity))
                    .onTapGesture {
                        self.isPresented = false
                    }
            }
        }
    }
}

We basically moved almost everything we had before from the naive way, to this BannerViewModifier.

And to make this accessible from any view, we just need to create an extension of View.

extension View {
    func banner(isPresented: Binding<Bool>, data: BannerData, action: (() -> Void)? = nil) -> some View {
        self.modifier(BannerViewModifier(isPresented: isPresented, data: data, action: action))
    }
}

This gives us the ability to show a banner from any View.. Any! So we might need to show one from our main container of a View, but we could also show a contextualised one next to a button for example.

Finally, if we go back to our initial approach we can update this to something way more elegant:

@State private var showBanner: Bool = false
@State private var countTap: Int = 0

var body: some View {
        Text(Some text”)
            .banner(isPresented: $showBanner, data: BannerData(title:View modifier war”, actionTitle:NICE, level: .warning), action: {
                self.countTap += 1
            })
}

Conclusion

Looking to what we have done, we have the ability from any feature/content to access to a nice and consice API that is highly re-usable.

It’s following SwiftUI principles using the tools we have been given. With ViewModifier we are also leveraging opaque return type, returning some View we hide the concrete implementation of a BannerView to allow us to update it without having changes on the feature side. That’s a huge benefit.

This is just an example of how we could use ViewModifier which can be handy and could be applied in many more places! If you find yourself repeating common layout in multiple places, use a function builder, extract it to its own view or use ViewModifier and have an extension on View.

Project on GitHub

Everything is available under a project here: https://github.com/thomas-sivilay/blog-notification-banner-swiftui


© Thomas Sivilay 2021, Built with Gatsby