I'm trying to apply some validation to a TextField to add a red border around the field when the content is invalid (in this case, I'm validating that the content is a positive number that is less than the specified maxLength
).
The validation works fine and the border is applied when the value is out of range. However, when the border is applied to the TextField, the TextField loses focus in the UI (and also loses focus when the border is removed).
Here is a snippet of my code (I've included some extensions I'm using, but I don't think those are relevant to the issue)
import Foundation
import SwiftUI
import Combine
struct MyView : View {
@Binding var value: Int
var label: String
var maxLength: Int
@State private var valid: Bool = true
var body: some View {
TextField(label, value: $value, format: .number)
.textFieldStyle(RoundedBorderTextFieldStyle())
.fixedSize()
.multilineTextAlignment(.trailing)
.onReceive(Just(value), perform: validate)
.if(!valid, transform: { $0.border(.red)})
}
func validate(val: Int) {
let newVal = val.clamped(to: 0...maxLength)
if newVal != val {
valid = false
} else {
valid = true
}
}
}
extension View {
@ViewBuilder
func `if`<Transform: View>(_ condition: Bool, transform: (Self) -> Transform) -> some View {
if condition { transform(self) }
else { self }
}
}
extension Comparable {
func clamped(to limits: ClosedRange<Self>) -> Self {
return min(max(self, limits.lowerBound), limits.upperBound)
}
}
Is there any way to preserve focus on the TextField when styles are conditionally applied to it?
Or am I approaching validation wrong altogether? Is there a better way to check fields and apply conditional styling?
CodePudding user response:
Because of the way your if
modifier is structured, SwiftUI is unable to see that the underlying View
is the same in the two conditions. For more detail on this, I'd suggest you watch Demystifying SwiftUI from WWDC 2021.
One solution is the simplify your border
modifier into the following:
.border(valid ? .clear : .red)
This way, SwiftUI can still tell that this is the same underlying View
.