La transición de UIKit a SwiftUI ha revolucionado la forma en que construimos interfaces de usuario en el ecosistema de Apple. Para cualquier iOS Developer moderno, dominar este framework declarativo es esencial. Sin embargo, en esta transición, algunos componentes que dábamos por sentados en UIKit requieren un enfoque diferente. Uno de los ejemplos más clásicos es la implementación de un botón clear en un TextField en SwiftUI.
En UIKit, lograr esto era tan sencillo como acceder a la propiedad clearButtonMode de un UITextField. En SwiftUI, Apple nos invita a pensar de manera más compositiva. Aunque el framework no proporciona un modificador nativo directo de un solo paso para esto en todas sus plataformas, nos ofrece herramientas increíblemente potentes para construir nuestra propia solución limpia, reactiva y reutilizable.
En este tutorial extenso y detallado, exploraremos paso a paso cómo crear e integrar un botón de borrado en tus campos de texto utilizando la programación Swift. Abordaremos la creación de la interfaz, la gestión del estado, la refactorización a través de ViewModifier, y cómo asegurar que nuestra solución funcione a la perfección en iOS, macOS y watchOS utilizando Xcode.
1. El Ecosistema Actual de la Programación Swift
Antes de sumergirnos en el código, es vital entender por qué SwiftUI aborda los problemas de UI de la manera en que lo hace. La programación Swift se ha decantado fuertemente por paradigmas declarativos y reactivos. En lugar de decirle al sistema cómo hacer los cambios (imperativo), le decimos al sistema cuál debe ser el estado final de la UI basado en los datos actuales.
Como iOS Developer, tu trabajo en Xcode ya no consiste en arrastrar elementos en un Storyboard y conectar @IBOutlet o @IBAction. Ahora construyes vistas ligeras que reaccionan a los cambios de estado (como @State o @Binding).
Esta filosofía es la que aplicaremos para construir nuestro botón clear en un TextField en SwiftUI. En lugar de buscar una propiedad oculta, compondremos vistas básicas (TextField, Button, Image) para crear una experiencia de usuario fluida y coherente.
2. Comprendiendo el TextField en SwiftUI
Un TextField en SwiftUI es un control que permite al usuario introducir texto de una sola línea. Su funcionamiento básico requiere un estado (State) que almacene el texto que el usuario está escribiendo.
Veamos el ejemplo más fundamental en Swift:
import SwiftUI
struct BasicTextFieldView: View {
@State private var userInput: String = ""
var body: some View {
TextField("Escribe tu nombre...", text: $userInput)
.textFieldStyle(.roundedBorder)
.padding()
}
}
En este código:
@Stateindica queuserInputes la fuente de la verdad para esta vista.- El símbolo
$en$userInputcrea un Binding (enlace bidireccional). Cuando el usuario escribe, la variable se actualiza; si la variable cambia desde el código, elTextFieldrefleja ese cambio de inmediato.
Esta naturaleza bidireccional es la clave para implementar nuestro botón de borrado. Si logramos que un botón cambie userInput a una cadena vacía "", el TextField se borrará automáticamente en la pantalla.
3. La Solución Básica: Usando un HStack
El enfoque más directo para añadir un botón clear en un TextField en SwiftUI es agrupar el campo de texto y un botón dentro de un HStack (Horizontal Stack) o usar un overlay.
Vamos a construir la primera iteración directamente en nuestra vista principal:
import SwiftUI
struct ClearableTextFieldBasico: View {
@State private var searchQuery: String = ""
var body: some View {
HStack {
TextField("Buscar en la app...", text: $searchQuery)
.padding(.vertical, 10)
.padding(.horizontal, 15)
// Botón Clear
if !searchQuery.isEmpty {
Button(action: {
// Acción para borrar el texto
self.searchQuery = ""
}) {
Image(systemName: "xmark.circle.fill")
.foregroundColor(.gray)
.padding(.trailing, 10)
}
}
}
.background(Color(UIColor.secondarySystemBackground))
.cornerRadius(10)
.padding()
}
}
Análisis del Código:
- Condicional
if !searchQuery.isEmpty: Una de las grandes ventajas de SwiftUI es su capacidad para renderizar vistas condicionalmente de forma muy limpia. El botón con la “X” solo aparecerá si hay texto en el campo. - SF Symbols: Usamos
Image(systemName: "xmark.circle.fill"). Apple proporciona miles de iconos vectoriales integrados que se comportan como texto y se escalan perfectamente. - Acción del Botón: Simplemente asignamos
""a nuestro@State. Gracias a la reactividad de Swift, la UI se actualiza al instante.
Aunque este enfoque funciona, si eres un iOS Developer trabajando en una aplicación real con múltiples pantallas, no querrás copiar y pegar este bloque de HStack cada vez que necesites un campo de texto con botón de borrado. Necesitamos refactorizar.
4. Refactorización Nivel 1: Creando un Componente Reutilizable
La regla de oro en el desarrollo con SwiftUI es la modularidad. Vamos a extraer esta lógica en un componente visual independiente que podamos instanciar en cualquier parte de nuestro proyecto en Xcode.
import SwiftUI
/// Un componente de TextField personalizado que incluye un botón de borrado automático.
struct ClearableTextFieldComponent: View {
// Propiedades de configuración
var placeholder: String
@Binding var text: String
var body: some View {
HStack {
TextField(placeholder, text: $text)
// Deshabilitamos el autocapitalize por defecto en búsquedas
.textInputAutocapitalization(.never)
.disableAutocorrection(true)
if !text.isEmpty {
Button(action: {
// Añadimos una sutil animación al borrar
withAnimation {
self.text = ""
}
}) {
Image(systemName: "multiply.circle.fill")
.foregroundColor(Color.secondary)
}
// Mejoramos el área de toque (hit target) para mejor UX
.buttonStyle(.plain)
}
}
.padding(8)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(Color(UIColor.systemGray6))
)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color.gray.opacity(0.3), lineWidth: 1)
)
}
}
¿Por qué usamos @Binding aquí?
A diferencia de @State (que maneja datos locales internos de la vista), @Binding permite que este componente modifique una variable que vive en la vista “padre” que lo llamó.
Cómo usarlo en tu app:
struct ContentView: View {
@State private var email: String = ""
var body: some View {
VStack(spacing: 20) {
Text("Inicia Sesión")
.font(.largeTitle)
// Usando nuestro componente reutilizable
ClearableTextFieldComponent(placeholder: "Correo electrónico", text: $email)
.padding(.horizontal)
Spacer()
}
.padding(.top)
}
}
Esta solución es robusta, pero altera la jerarquía de vistas. Estamos envolviendo el TextField dentro de otras estructuras de diseño. ¿Qué pasa si queremos aplicar esto como un simple modificador nativo de SwiftUI?
5. La Solución Definitiva (Modo Pro): Usando ViewModifier
Para un iOS Developer senior, la forma más “idiomática” o natural de implementar un botón clear en un TextField en SwiftUI es a través de un ViewModifier.
Los modificadores de vista nos permiten adjuntar comportamientos e interfaces adicionales a vistas existentes, manteniendo la sintaxis fluida (encadenamiento de puntos) característica de SwiftUI.
Paso 5.1: Crear el ViewModifier
import SwiftUI
struct ClearButtonModifier: ViewModifier {
@Binding var text: String
func body(content: Content) -> some View {
ZStack(alignment: .trailing) {
content
if !text.isEmpty {
Button(action: {
self.text = ""
}) {
Image(systemName: "xmark.circle.fill")
.foregroundColor(Color(UIColor.opaqueSeparator))
}
.padding(.trailing, 8) // Ajuste para que no quede pegado al borde
// Evitamos que el botón tome el foco en macOS/tvOS inesperadamente
.buttonStyle(PlainButtonStyle())
}
}
}
}
Paso 5.2: Extender la vista (Extension View)
Para que nuestro modificador se use exactamente igual que los modificadores nativos de Apple en Xcode, creamos una extensión sobre View.
extension View {
/// Añade un botón de borrado a cualquier vista que contenga un texto ligado (Binding).
func clearButton(text: Binding<String>) -> some View {
self.modifier(ClearButtonModifier(text: text))
}
}
Paso 5.3: El Resultado Final en Uso
Ahora mira lo increíblemente limpio que queda el código de nuestra interfaz:
struct BúsquedaAvanzadaView: View {
@State private var textoBusqueda: String = ""
var body: some View {
VStack {
TextField("Busca tu artista favorito...", text: $textoBusqueda)
.padding()
// Aquí usamos nuestro modificador personalizado
.clearButton(text: $textoBusqueda)
.background(Color.gray.opacity(0.1))
.cornerRadius(10)
.padding()
}
}
}
Esta es la forma preferida en la programación Swift moderna. Mantiene la declaración del TextField como la vista principal y simplemente le “inyecta” la funcionalidad del botón de borrado mediante una superposición (ZStack dentro del modifier).
6. Consideraciones Multiplataforma: iOS, macOS y watchOS
Como desarrollador en el ecosistema Apple, rara vez escribes código para un solo dispositivo. SwiftUI promete “Aprender una vez, aplicar en cualquier lugar”. Veamos cómo se comporta nuestra solución en diferentes plataformas utilizando Xcode.
En iOS y iPadOS
Nuestra solución de ViewModifier funciona perfectamente aquí. La única consideración de diseño es asegurar que el “hit target” (el área táctil) del botón de borrado sea al menos de 44×44 puntos, según las Human Interface Guidelines de Apple. Podríamos añadir un .frame(width: 44, height: 44) alrededor del Image dentro de nuestro botón para garantizar esto.
En macOS
En el Mac, los usuarios interactúan con un ratón o trackpad. El TextField nativo de macOS tiene un estilo visual diferente. Si usamos nuestro ClearButtonModifier, debemos tener cuidado con el .textFieldStyle().
#if os(macOS)
TextField("Buscar archivos...", text: $busqueda)
.textFieldStyle(.roundedBorder)
// En macOS, a veces es útil añadir un efecto onHover
.clearButton(text: $busqueda)
#endif
En macOS, el PlainButtonStyle() que incluimos en el paso 5.1 es crucial, ya que los botones estándar en macOS tienen bordes y fondos pesados por defecto.
En watchOS
El Apple Watch tiene un espacio de pantalla extremadamente limitado y métodos de entrada únicos (Scribble, Dictado, Teclado QWERTY en modelos Ultra/Series 7+). Añadir un botón dentro de un TextField en watchOS puede consumir un espacio horizontal valiosísimo. En muchos casos, en watchOS es preferible omitir el botón de borrado integrado en favor de permitir que el usuario use el botón físico “Borrar” del teclado nativo del reloj.
Si decides incluirlo, debes reducir sustancialmente los paddings en tu modificador usando compilación condicional:
#if os(watchOS)
.padding(.trailing, 2)
#else
.padding(.trailing, 8)
#endif
7. Buenas Prácticas: Accesibilidad y Animaciones
Un iOS Developer de alto nivel no solo hace que la aplicación luzca bien, sino que garantiza que sea utilizable por todos.
Accesibilidad (VoiceOver)
Si no le decimos a VoiceOver qué hace ese ícono de la “X”, un usuario con discapacidad visual solo escuchará “Botón”. Debemos añadir contexto.
Volvamos a nuestro botón dentro del ViewModifier y mejoremos el código:
Button(action: {
self.text = ""
}) {
Image(systemName: "xmark.circle.fill")
.foregroundColor(Color.secondary)
}
.accessibilityLabel("Borrar texto")
.accessibilityHint("Borra todo el contenido del campo de texto de búsqueda.")
Con esto, VoiceOver leerá: “Borrar texto, botón. Borra todo el contenido del campo de texto de búsqueda.” Esto mejora la experiencia del usuario de manera exponencial.
Animaciones Fluidas
Cuando el texto pasa de estar vacío a tener un carácter (o viceversa), el botón aparece o desaparece. Si este cambio es abrupto, la interfaz se sentirá poco refinada.
Añadir una transición resuelve esto en SwiftUI:
if !text.isEmpty {
Button(action: {
withAnimation(.easeInOut(duration: 0.2)) {
self.text = ""
}
}) {
Image(systemName: "xmark.circle.fill")
}
.transition(.opacity.combined(with: .scale))
}
Nota: Para que la transición surta efecto al escribir, a veces es necesario aplicar un .animation(.default, value: text) al contenedor.
8. Optimizando el Flujo de Trabajo en Xcode (Previews)
Xcode ofrece una característica fantástica llamada Canvas/Previews. Al desarrollar componentes de UI reutilizables como este, debemos crear previsualizaciones robustas que nos permitan probar el componente sin compilar en el simulador.
struct ClearableTextField_Previews: PreviewProvider {
// Usamos un wrapper para manejar el @State en la Preview
struct PreviewWrapper: View {
@State private var textWithContent = "SwiftUI Rocks"
@State private var emptyText = ""
var body: some View {
VStack(spacing: 30) {
Text("Previsualizaciones")
.font(.headline)
TextField("Con contenido...", text: $textWithContent)
.textFieldStyle(.roundedBorder)
.clearButton(text: $textWithContent)
TextField("Sin contenido...", text: $emptyText)
.textFieldStyle(.roundedBorder)
.clearButton(text: $emptyText)
}
.padding()
.previewLayout(.sizeThatFits)
}
}
static var previews: some View {
PreviewWrapper()
// Probamos también en modo oscuro
PreviewWrapper()
.preferredColorScheme(.dark)
}
}
Configurar estas previews te ahorrará horas de compilación y te garantizará que tu implementación de la programación Swift reacciona bien ante diferentes estados y modos (claro/oscuro).
9. Una Alternativa: Integrando UIKit con UIViewRepresentable
Aunque el objetivo principal es hacer todo de forma nativa en SwiftUI, como iOS Developer debes saber que existe un “Plan B”. Si estrictamente necesitas replicar el comportamiento 100% exacto del UITextField de UIKit (por ejemplo, comportamientos específicos del teclado o integraciones heredadas), puedes envolver un UITextField usando UIViewRepresentable.
Aquí tienes un vistazo rápido de cómo sería esa integración:
import SwiftUI
import UIKit
struct LegacyTextField: UIViewRepresentable {
var placeholder: String
@Binding var text: String
func makeUIView(context: Context) -> UITextField {
let textField = UITextField()
textField.placeholder = placeholder
textField.borderStyle = .roundedRect
// ¡La magia de UIKit! Un botón clear nativo en una línea.
textField.clearButtonMode = .whileEditing
textField.delegate = context.coordinator
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = text
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITextFieldDelegate {
var parent: LegacyTextField
init(_ parent: LegacyTextField) {
self.parent = parent
}
func textFieldDidChangeSelection(_ textField: UITextField) {
parent.text = textField.text ?? ""
}
}
}
¿Cuándo usar este método? Generalmente, nunca a menos que tengas un requisito muy específico de negocio que no puedas lograr con la aproximación pura de SwiftUI. Envolver UIKit añade sobrecarga de rendimiento y código (coordinadores, delegados) que rompen la pureza de tu arquitectura declarativa. El método del ViewModifier (Sección 5) es superior en el 99% de los casos.
10. Conclusión y Siguientes Pasos
Construir un botón clear en un TextField en SwiftUI es mucho más que simplemente añadir una equis a la pantalla. Es una excelente oportunidad para comprender profundamente la programación Swift moderna.
A lo largo de este tutorial, hemos visto cómo:
- Declarar estados y enlazar datos bidireccionalmente (
@Statey@Binding). - Componer vistas complejas a partir de formas primitivas (
HStack,ZStack). - Refactorizar código repetitivo en Componentes Customizados y
ViewModifiers. - Asegurar la Accesibilidad para que la app sea inclusiva.
- Preparar la UI para el ecosistema completo usando las directivas condicionales en Xcode.
Ser un excelente iOS Developer hoy en día significa dominar la abstracción. Al encapsular este comportamiento del botón clear en un modificador de extensión, has enriquecido tu propia librería personal de herramientas. La próxima vez que inicies un proyecto en Xcode, simplemente arrastrarás tu archivo ClearButtonModifier.swift al proyecto y tendrás esta funcionalidad lista para usarse en toda tu aplicación en segundos.








