How can I reverse an animation that is triggered by a watched variable?

Hi guys,
I have a problem I'm trying to solve and I can't figure out a way to do it. I have a variable in the view model that is being watched and will flash the background of a textfield red if an invalid character is entered. (This is a business requirement for a SwiftUI proof of concept at work so I cannot change this behavior.). Unfortuantely I cannot figure out how to do it.
The code below is a simplification of what I need. If you enter a "!" in the text field, the background will render red. The issue here is because it can't change back to white without a state change, the background stays red until a new character is pressed.
I'm trying to figure out a workaround to this since it doesn't seem possible to chain animations and the "repeatCount(1, autoreverses: true) doesn't do anything here. Anyone have any suggestions?

class Person: ObservableObject {
    @Published var name: String = "" { willSet { processValue(value: newValue) }}
    @Published var hasError: Bool = false
    
    private func processValue(value: String) {
        value.last == "!" ? (hasError = true) : (hasError = false)
    }
}

struct ContentView: View {
    @ObservedObject var vm = Person()
    
    @State private var change = false
    
    var body: some View {
        VStack {
            TextField("Name", text: $vm.name)
            .background(vm.hasError ? Color.red : Color.white) //I'd rather just flash this red and turn it back to white
                .animation(Animation.linear(duration: 0.2).repeatCount(1, autoreverses: true))
        }.padding()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
Accepted Answer

Essentially what you're looking to do is to reverse the 'hasError' property after a certain amount of time. There are several ways to do this, the simplest being a basic dispatch:


DispatchQueue.main.async { self.vm.hasError = false }


A more robust solution would be to use a Publisher though, via a PassthroughSubject. That will let you set delays and even debounce the events (coalescing nearby events together). For a true 'flash' effect, I'd recommend setting the background color to red instantly, without animation, and then animating the fade back to the original color (or to Clear, which is what a TextField would use normally).


Here's my implementation of a ContentView that would work with your Person class to implement a decent-looking background flash:


struct ContentView: View {
    @ObservedObject var vm = Person()
    @State var color = Color.clear
    
    private var unflash = PassthroughSubject<Void, Never>()
    private var afterUnflash: AnyPublisher<Void, Never>
    
    init() {
        self.afterUnflash = unflash
            .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
            .eraseToAnyPublisher()
    }
    
    var body: some View {
        VStack {
            TextField("Name", text: $vm.name)
                .padding()
                .background(color)
                .onReceive(vm.$hasError, perform: {
                    if $0 {
                        self.color = .red
                        self.unflash.send(())
                    } else {
                        withAnimation(.linear(duration: 0.2)) {
                            self.color = .clear
                        }
                    }
                })
                .onReceive(afterUnflash, perform: { _ in
                    self.vm.hasError = false
                })
        }
    }
}


I've added a Color state value which I'm using to set the background, followed by a PassthroughSubject and a debouncing publisher derived from that. I then monitor the publisher for the vm.hasError property, and when the value becomes true I set the color to red and trigger my PassthroughSubject; when the value becomes false I use an animation to set the color back to clear. Lastly, I monitor the debounced publisher and set vm.hasError to false when it fires. The debouncing here is used to coalesce multiple signals together; try commenting out line ten and typing a series of "!" and you'll see the color switching back & forth rapidly. With the debounce in there, it will only send the signal on when 300 milliseconds of time has elapsed since the last one was received, giving a better overall appearance—it'll stay red while you quickly type "!" and fade out only once you let go.

Thanks so much!

How can I reverse an animation that is triggered by a watched variable?
 
 
Q