lukecsmith.co.uk

A SwiftUI custom alert using ViewModifiers and transitions

Luke, 03 Feb 2021

Custom-Alert

I was recently in need of a pop-up error message, which had a custom design not in the normal library. But I wanted to use it like I can use things like .actionSheet – using a ViewModifier so its easy to apply to any View. I wanted to make use of SwiftUI transitions, so that the error will slide and fade gracefully in to and out of view using a single modifier. I wanted the error view to appear for a fixed amount of time before disappearing (rather than being dismissed). The question i struggled to answer for a while, was how best to instruct the error view to appear. Should I have an @State boolean that is true to make the view appear, and then false? How then would a timer work so that the view disappears after a few seconds?

I tried allsorts of things and nothing seemed quite right, until I stumbled upon the .onChange modifier. This can be used as an observer of an @State property, allowing you to run a closure based on an updated value. Ultimately then I just needed one property to associate with my error view, an @State String – and I simply observed that for changes. On a change, I could trigger my animation to slide the view in and also fade the opacity, so that the new error message could appear.

So first, heres how the view is called. You just need one @State String property in your view, and the .errorView ViewModifier on it. To call for the error to appear, you just set a string on your String property and it will make the error view appear. Such as :

struct DemoErrorView: View {
    
    @State var errorText = ""
    
    var body: some View {
        ZStack {
            VStack(spacing: 20.0) {
                Button("Error 1") {
                    self.errorText = "First Error"
                }
                Button("Error 2") {
                    self.errorText = "Second Error"
                }
                Button("Error 3") {
                    self.errorText = "Third Error"
                }
            }
            .errorView(text: $errorText)
        }
    }
}

Turning a view into ViewModifier (so that you can use it as .errorView – rather than actually sepcifying the view itself) is a standard operation, but I’ll quickly show you here how I did it for this ErrorView:

extension View {
    func errorView(text: Binding<String>) -> some View {
        self.modifier(ErrorViewModifier(text: text))
    }
}

struct ErrorViewModifier: ViewModifier {
    
    @Binding var text: String
    func body(content: Content) -> some View {
        ZStack {
            content
            ErrorView(text: $text)
        }
    }
}

So the above function definition equates to how our ViewModifier will look when applied, complete with the variables that will be passed – in our case an @Binding property that links back to our @State string property above. This is how we pass the error text to the error view.

Finally, heres the ErrorView itself. Ive added various explanatory comments within.

/**
 The error text is passed in via the @Binding text property.  when passed in, the .onChange closure is fired below, which sets the visibleErrorText property with the incoming text, in turn making the red text box appear and show the text.  At this time a timer starts, which will clear the visible text and the passed in text property too so that the red box disappears.
 */
struct ErrorView: View {
    
    @Binding var text: String
    @State var visibleErrorText: String = ""
    let padding: CGFloat = 5.0
    @State var timer: Timer?
    
    var body: some View {
        VStack {
            //only show the error view if the visibleErrorText String is not empty:
            if visibleErrorText.count > 0 {
                HStack(alignment: .center) {
                    Spacer()
                    Text(visibleErrorText)
                        .foregroundColor(Color.white)
                        .multilineTextAlignment(.center)
                        .padding(15)
                    Spacer()
                }
                .background(Color.red)
                .cornerRadius(10)
                .shadow(radius: 24)
                .padding(.vertical, 5)
                .padding(.horizontal, 24)
                //this transition dictates how the box appears and disappears.  The animation is described in .onChange below
                .transition(.asymmetric(insertion: AnyTransition.opacity
                                                .combined(with: AnyTransition.move(edge: .top)),
                                            removal: AnyTransition.opacity
                                                .combined(with: AnyTransition.move(edge: .top))))
            }
            Spacer()
            //check the @Binding text property for changes.  if one comes in, set it on our visibleErrorText property
            //then create a timer to make the errorview disappear in 3 seconds.
        }.onChange(of: text) { newValue in
            withAnimation {
                self.visibleErrorText = newValue
                self.timer?.invalidate()
                self.timer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) { _ in
                    withAnimation {
                        self.visibleErrorText = ""
                        self.text = ""
                    }
                }
            }
        }
    }
}

For once Ive shared this on a github (along with a few other animation examples – Im going to build this project up with nice SwiftUI animation stuff as I find it). Improvements or suggestions welcome. My SwiftUI Animation Examples are here

Tagged with: