2 Replies
      Latest reply on Jan 28, 2020 4:16 PM by SN81
      SN81 Level 1 Level 1 (0 points)

        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()
            }
        }
        
        • Re: How can I reverse an animation that is triggered by a watched variable?
          Jim Dovey Level 3 Level 3 (350 points)

          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.