Reputation: 652
I am trying to add a ClearButton to TextField in SwiftUI when the particular TextField is selected.
The closest I got was creating a ClearButton
ViewModifier
and adding it to the TextField
using .modifer()
The only problem is ClearButton
is permanent and does not disappear when TextField
is deselected
TextField("Some Text" , text: $someBinding).modifier(ClearButton(text: $someBinding))
struct ClearButton: ViewModifier {
@Binding var text: String
public func body(content: Content) -> some View {
HStack {
content
Button(action: {
self.text = ""
}) {
Image(systemName: "multiply.circle.fill")
.foregroundColor(.secondary)
}
}
}
}
Upvotes: 46
Views: 24611
Reputation: 5665
After experimenting with different solutions and checking out some of the answers here, I ended up combining a few ideas to get something that works today (2024). It mimics the UITextField
clear button with smooth animations, while still supporting older APIs like iOS 14. Checking focus became easier with @FocusState
but unfortunately that requires iOS15.
If you're like me you probably felt that using using UIViewRepresentable
from UIKit is overkill for just adding a clear button. I wanted to stick with SwiftUI and keep things simple.
import SwiftUI
/// A custom TextField with a clear button that mimics iOS 15+ behavior, but works with iOS 14.
struct ClearableTextField: View {
let placeholder: String
@Binding var text: String
@State private var isTextFieldFocused: Bool = false
var body: some View {
HStack {
TextField(placeholder, text: $text, onEditingChanged: { isEditing in
isTextFieldFocused = isEditing
})
Image(systemName: "xmark.circle.fill")
.foregroundColor(Color(UIColor.tertiaryLabel)) // Semantic colors for light/dark mode
.opacity(!text.isEmpty && isTextFieldFocused ? 1 : 0) // Fade animation
.onTapGesture { text = "" }
.animation(.easeInOut(duration: 0.2), value: text)
.animation(.easeInOut(duration: 0.2), value: isTextFieldFocused)
}
}
}
}
#Preview {
@State var text = "Example text"
Form {
ClearableTextField(placeholder: "Enter text here", text: $text)
}
}
UIViewRepresentable
.If you can update your minimum iOS version, definitely: do that! But if you’re still supporting iOS 14, hope this helps!
Upvotes: 0
Reputation: 1459
Use ZStack
to position the clear button appear inside the TextField
.
TextField("Some Text" , text: $someBinding).modifier(ClearButton(text: $someBinding))
struct ClearButton: ViewModifier
{
@Binding var text: String
public func body(content: Content) -> some View
{
ZStack(alignment: .trailing)
{
content
if !text.isEmpty
{
Button(action:
{
self.text = ""
})
{
Image(systemName: "delete.left")
.foregroundColor(Color(UIColor.opaqueSeparator))
.padding(.trailing, 8)
.padding(.vertical, 8)
.contentShape(Rectangle())
}
}
}
}
}
Upvotes: 48
Reputation: 1010
Works in iOS 18.0 too
Recently I have noticed in some older solutions that the clear button was not tappable, rather it was triggering the textField's text. The following solution fixes the problem:
// Make a ViewModifier
import SwiftUI
struct TextFieldClearButton: ViewModifier {
@Binding var text: String
func body(content: Content) -> some View {
ZStack(alignment: .trailing) {
content
Button {
text = ""
} label: {
Image(systemName: "multiply.circle.fill")
}
.opacity(text.isEmpty ? 0 : 1)
.buttonStyle(.plain)
.foregroundColor(.secondary)
.padding(.trailing, 4)
}
}
}
extension TextField {
func clearButton(on text: Binding<String>) -> some View {
modifier(TextFieldClearButton(text: text))
}
}
Use in your TextField
's:
TextField("Add your note", text: $text)
.clearButton(on: $text)
// Here we have a `@State var text: String = ""` which is bind to the TextField
Mainly, making the clear button to buttonStyle(.plain)
fixes the tap not triggering issue on the older solutions.
Upvotes: 0
Reputation: 6243
@_spi(Advanced) import SwiftUIIntrospect
TextField("", text: $text)
.introspect(.textField, on: .iOS(.v13...)) { textField in
textField.clearButtonMode = .whileEditing
}
ViewModifier
public struct ClearButton: ViewModifier {
@Binding var text: String
public init(text: Binding<String>) {
self._text = text
}
public func body(content: Content) -> some View {
HStack {
content
Spacer()
Image(systemName: "multiply.circle.fill")
.foregroundColor(.secondary)
.opacity(text == "" ? 0 : 1)
.onTapGesture {
self.text = ""
} // onTapGesture or plainStyle button
}
}
}
Usage:
@State private var name: String
...
Form {
Section() {
TextField("NAME", text: $name)
.modifier(ClearButton(text: $name))
}
}
UITextField.appearance().clearButtonMode = .whileEditing
Upvotes: 32
Reputation: 3225
For a clean Textfield in SwiftUI, with no overlapping between user inputted text and clear button, here an example:
ZStack(alignment: .trailing)
{
TextField("Tapez une url", text: $myViewModel.urlString, onCommit: {
// action
})
.submitLabel(.done)
.disableAutocorrection(true)
.padding(.trailing, 30)
.padding(.top, 3)
.padding(.leading, 5)
.frame(height: 30, alignment: .top)
.background(.white, in: .rect(cornerRadius: 5))
.overlay(
Button(action: {
myViewModel.urlString = ""
}) {
Image(systemName: "delete.left")
.opacity(myViewModel.urlString.isEmpty ? 0 : 1)
.symbolRenderingMode(.palette)
.foregroundColor(.black)
}
.padding(.trailing, 5),
alignment: .trailing
)
}
Upvotes: 0
Reputation: 81
If you want to have multiple TextField and also want to tap on the screen and hide the proper clear button you can do like this:
struct ClearTextFiledButton: ViewModifier {
@Binding var text: String
@FocusState private var showDeleteButton: Bool
func body(content: Content) -> some View {
content
.focused($showDeleteButton)
.overlay(alignment: .trailing) {
if showDeleteButton {
Button {
text = ""
} label: {
Image(systemName: "xmark.circle.fill").foregroundColor(Color.gray)
}
.padding(.trailing,8)
.opacity(text.isEmpty ? 0 : 1)
}
}
}
}
extension View {
func showClearButton(_ text: Binding<String>) -> some View {
return self.modifier(ClearTextFiledButton(text: text))
}
}
Upvotes: 0
Reputation: 210
Its simple to Just do like this
TextField("Please enter", text: $viewModel.label).onAppear { UITextField.appearance().clearButtonMode = .whileEditing }
Upvotes: 0
Reputation: 109
I found this answer from @NigelGee on "Hacking with Swift".
.onAppear {
UITextField.appearance().clearButtonMode = .whileEditing
}
It really helped me out.
Upvotes: 7
Reputation: 658
Simplest solution I came up with
//
// ClearableTextField.swift
//
// Created by Fred on 21.11.22.
//
import SwiftUI
struct ClearableTextField: View {
var title: String
@Binding var text: String
init(_ title: String, text: Binding<String>) {
self.title = title
_text = text
}
var body: some View {
ZStack(alignment: .trailing) {
TextField(title, text: $text)
Image(systemName: "xmark.circle.fill")
.foregroundColor(.secondary)
.onTapGesture {
text = ""
}
}
}
}
struct ClearableTextField_Previews: PreviewProvider {
@State static var text = "some value"
static var previews: some View {
Form {
// replace TextField("Original", text: $text) with
ClearableTextField("Clear me", text: $text)
}
}
}
Upvotes: 2
Reputation: 7636
Use .appearance()
to activate the button
var body: some View {
UITextField.appearance().clearButtonMode = .whileEditing
return TextField(...)
}
For reuse try with this:
func TextFieldUIKit(text: Binding<String>) -> some View{
UITextField.appearance().clearButtonMode = .whileEditing
return TextField("Nombre", text: text)
}
Upvotes: 41
Reputation: 3595
Not exactly what you're looking for, but this will let you show/hide the button based on the text
contents:
HStack {
if !text.isEmpty {
Button(action: {
self.text = ""
}) {
Image(systemName: "multiply.circle")
}
}
}
Upvotes: 5
Reputation: 119214
You can add another Binding
in your modifier
:
@Binding var visible: Bool
then bind it to opacity of the button:
.opacity(visible ? 1 : 0)
then add another State
for checking textField
:
@State var showClearButton = true
And lastly update the textfield:
TextField("Some Text", text: $someBinding, onEditingChanged: { editing in
self.showClearButton = editing
}, onCommit: {
self.showClearButton = false
})
.modifier( ClearButton(text: $someBinding, visible: $showClearButton))
Upvotes: 7