I built a custom textfield with the following additional functionality:
The button action sets my text binding to ""
When I enter text into the textfield, the binding triggers properly.
But when I tap on the button to clear the text, the binding does not trigger.
Is there a reason for this?
Here is my struct. See the comments for where the issues are occurring.
public struct CustomTextField: View {
// MARK: - Property Wrappers
@Environment(\.isEnabled) private var isEnabled: Bool
@Binding private var text: String
@Binding private var errorText: String
@State private var secureTextHidden: Bool = true
// MARK: - Struct Properties
private var placeholder: String
private var isSecure: Bool
private var hasLabel: Bool = false
private var hasErrorLabel: Bool = false
private var onChange: ((String) -> Void)?
// MARK: - Computed Properties
private var borderColor: Color {
guard isEnabled else {
return .gray
}
if !errorText.isEmpty {
return .red
} else if !text.isEmpty {
return .blue
} else {
return .gray
}
}
private var textColor: Color {
guard isEnabled else {
return Color.gray.opacity(0.5)
}
return .black
}
private var textField: some View {
let binding = Binding<String> {
self.text
} set: {
// This is triggered correctly when text changes, but not when text is changed within my button action.
self.text = $0
onChange?($0)
}
if isSecure && secureTextHidden {
return SecureField(placeholder, text: binding)
.eraseToAnyView()
} else {
return TextField(placeholder, text: binding)
.eraseToAnyView()
}
}
private var hasText: Bool { !text.isEmpty }
private var hasError: Bool { !errorText.isEmpty }
// MARK: - Init
/// Initializes a new CustomTextField
/// - Parameters:
/// - placeholder: the textfield placeholder
/// - isSecure: if true, textfield will behave like a password field
/// - hasLabel: Show placeholder as a label when text is entered
/// - hasErrorLabel: Visible Error Label underneath
/// - onChange: code that will run on each keystroke (optional)
public init(
placeholder: String,
text: Binding<String>,
errorText: Binding<String> = .constant(""),
isSecure: Bool = false,
hasLabel: Bool = false,
hasErrorLabel: Bool = false,
onChange: ((String) -> Void)? = nil
) {
self.placeholder = placeholder
_text = text
_errorText = errorText
self.isSecure = isSecure
self.hasLabel = hasLabel
self.hasErrorLabel = hasErrorLabel
self.onChange = onChange
}
// MARK: - Body
public var body: some View {
VStack(alignment: .leading, spacing: .textMargin) {
if hasLabel {
Text("\(placeholder)")
.foregroundColor(textColor)
.offset(
x: hasText ? 0 : 16,
y: hasText ? 0 : 30
)
.opacity(hasText ? 1 : 0)
.animation(.easeOut(duration: 0.3))
} else {
EmptyView()
}
HStack(alignment: .center, spacing: 8) {
ZStack(alignment: Alignment(horizontal: .trailing, vertical: .center)) {
HStack(alignment: .center, spacing: 16) {
textField
HStack(alignment: .center, spacing: 16) {
if hasText && isEnabled {
Button {
text = ""
// I had to trigger onChange manually as setting my text above is not triggering my binding block.
onChange?(text)
} label: {
Image(systemName: "xmark.circle.fill")
}
.buttonStyle(BorderlessButtonStyle())
.foregroundColor(.black)
}
if isSecure && isEnabled {
Button {
secureTextHidden.toggle()
} label: {
Image(systemName: secureTextHidden ? "eye.fill" : "eye.slash.fill")
}
.buttonStyle(BorderlessButtonStyle())
.foregroundColor(Color.gray.opacity(0.5))
}
}
}
}
.padding(.margin)
.frame(height: .textFieldHeight, alignment: .center)
.background(
ZStack {
RoundedRectangle(cornerRadius: 4)
.fill(.white)
RoundedRectangle(cornerRadius: 4)
.strokeBorder(borderColor, lineWidth: 2)
}
)
}
if hasErrorLabel && isEnabled {
Text(errorText)
.lineLimit(2)
.font(.caption)
.foregroundColor(.red)
.offset(y: hasError ? 0 : -.textFieldHeight)
.opacity(hasError ? 1 : 0)
.animation(.easeOut(duration: 0.3))
} else {
EmptyView()
}
}
.foregroundColor(textColor)
.onAppear {
if hasText {
onChange?(text)
}
}
}
}
Changing text
binding doesn't affect binding
binding that is passed to the textfield. You should know the relationship between the two bindings correctly. set:
block is only triggered when binding
has been changed by the textfield.
If your minimum target iOS is 14, you can also use onChange
modifier, not your handler, to observe any change of text
binding in order to call your onChange
as the binding changed. And, you don't have to make binding
binding for the textfield. just pass text
binding directly.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.