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()
}
}
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.