Dominando los Campos de Texto Multilínea en SwiftUI: De TextField a TextEditor
SwiftUI ha revolucionado la forma en que construimos interfaces en el ecosistema de Apple. Sin embargo, una de las tareas más comunes, crear un simple campo de texto que acepte múltiples líneas, ha tenido una evolución interesante. El TextField básico que todos conocemos está diseñado para una sola línea de entrada, como un nombre de usuario o un asunto de correo electrónico.
Pero, ¿qué pasa cuando necesitas que tu usuario escriba una biografía, una nota larga o un comentario detallado?
En este tutorial exhaustivo, exploraremos las dos formas principales de manejar la entrada de texto multilínea en SwiftUI. Primero, veremos la solución moderna y preferida introducida en iOS 16: usar TextField con un eje vertical. Segundo, profundizaremos en la solución clásica y más robusta para grandes bloques de texto, disponible desde iOS 14: el TextEditor.
Cubriremos todo, desde la implementación básica hasta la personalización avanzada, cómo agregar placeholders, limitar caracteres y manejar el foco del teclado.
El Dilema: ¿TextField(axis: .vertical) o TextEditor?
Antes de escribir código, es crucial entender qué herramienta usar. La elección depende de tu caso de uso y de la versión de iOS que necesites soportar.
TextField(axis: .vertical)(iOS 16+): Esta es la evolución delTextFieldestándar. No es un editor de texto completo, sino más bien un campo de texto que crece dinámicamente con el contenido del usuario. Piensa en un campo de chat o un cuadro de comentarios de redes sociales. Comienza pequeño (una línea) y se expande verticalmente a medida que el usuario escribe, hasta un límite que puedes definir.TextEditor(iOS 14+): Este es el “peso pesado”. Es el equivalente directo deUITextViewde UIKit. Está diseñado para manejar grandes bloques de texto y es desplazable (scrollable) por defecto. Piensa en la app de Notas de Apple. Es ideal para cualquier cosa que supere unas pocas frases.
Solución Moderna (iOS 16+): TextField con Eje Vertical
Si tu app tiene como objetivo mínimo iOS 16, esta es la forma más sencilla y elegante de crear un campo de texto que se expande.
Implementación Básica
Comencemos con el ejemplo más simple. Solo necesitas un @State para almacenar el texto y el inicializador de TextFieldque acepta un axis.
import SwiftUI
struct ExpandingTextFieldView: View {
@State private var messageText: String = ""
var body: some View {
NavigationStack {
VStack {
TextField("Escribe tu mensaje...",
text: $messageText,
axis: .vertical) // ¡Aquí está la magia!
.textFieldStyle(.roundedBorder)
.padding()
Spacer()
}
.navigationTitle("Chat Rápido")
}
}
}
#Preview {
ExpandingTextFieldView()
}Al ejecutar esto, verás un TextField normal. Pero a medida que escribes y llegas al final de la línea, en lugar de detenerse, el campo crecerá verticalmente para acomodar la nueva línea.
Controlando la Expansión: .lineLimit()
Por defecto, el TextField crecerá indefinidamente. Esto puede ser un problema para tu diseño. Afortunadamente, podemos usar el modificador .lineLimit() para controlar este comportamiento.
.lineLimit() puede tomar un número exacto o un rango.
1. Límite Fijo: Esto le dice al TextField que crezca hasta 5 líneas. Después de eso, el campo dejará de crecer y se volverá desplazable internamente.
TextField("Escribe tu comentario (máx. 5 líneas)...",
text: $messageText,
axis: .vertical)
.lineLimit(5) // Crece hasta 5 líneas, luego hace scroll
.textFieldStyle(.roundedBorder)
.padding()2. Límite de Rango (Más Potente): Esta es la opción más flexible. Puedes definir un rango de líneas. El campo de texto siempre ocupará al menos 3 líneas y se expandirá hasta un máximo de 10.
@State private var userBio: String = ""
// ... en el body ...
TextField("Cuéntanos sobre ti...",
text: $userBio,
axis: .vertical)
.lineLimit(3...10) // Ocupa de 3 a 10 líneas
.padding()
.background(Color(.systemGray6))
.cornerRadius(10)
.padding(.horizontal)Este es increíblemente útil para formularios donde quieres que el campo de texto tenga un tamaño inicial razonable, pero también permita más entrada si es necesario.
Solución Clásica (iOS 14+): TextEditor
Si necesitas soportar versiones anteriores a iOS 16 o si necesitas un editor de texto completo y desplazable (como para una app de notas), TextEditor es tu elección.
Sin embargo, TextEditor viene con dos grandes advertencias:
- No tiene placeholder nativo.
- Por defecto, tiene un fondo transparente y no respeta los estilos de
TextField.
Veamos cómo solucionar ambos problemas.
Implementación Básica
Primero, lo básico. Es tan simple de inicializar como un TextField.
import SwiftUI
struct NotesEditorView: View {
@State private var notesBody: String = ""
var body: some View {
NavigationStack {
VStack {
TextEditor(text: $notesBody)
.padding()
}
.navigationTitle("Mis Notas")
}
}
}Al ejecutar esto, verás… casi nada. Solo un cursor parpadeando. Puedes escribir y se desplazará, pero no hay bordes ni fondo. Y lo más importante, no hay un “placeholder” que le diga al usuario qué hacer.
El Gran Problema: Añadir un Placeholder
La forma canónica de agregar un placeholder a un TextEditor es usando un ZStack. Colocamos el TextEditor en el fondo y un Text (nuestro placeholder) encima. Luego, simplemente ocultamos el Text cuando el TextEditor ya no está vacío.
struct TextEditorWithPlaceholder: View {
@State private var notesBody: String = ""
var body: some View {
NavigationStack {
ZStack(alignment: .topLeading) {
// 1. El TextEditor
TextEditor(text: $notesBody)
.padding(4) // Un poco de padding interno
// 2. El Placeholder
if notesBody.isEmpty {
Text("Escribe tus notas aquí...")
.font(.body) // Asegúrate de que coincida con la fuente del TextEditor
.foregroundColor(Color(.placeholderText))
.padding(8) // Ajusta para que coincida con el padding del TextEditor
.allowsHitTesting(false) // Permite que los toques "atraviesen" el texto
}
}
.frame(minHeight: 100, maxHeight: 300) // Dale un tamaño
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.gray, lineWidth: 1)
)
.padding()
.navigationTitle("Notas con Placeholder")
}
}
}Desglosemos las partes clave de esta solución:
ZStack(alignment: .topLeading): Alinea todo en la esquina superior izquierda.if notesBody.isEmpty: Esta es la lógica principal. El placeholder solo se muestra si elStringestá vacío.foregroundColor(Color(.placeholderText)): Usa el color de placeholder semántico del sistema para que se vea correcto en modo claro y oscuro..allowsHitTesting(false): ¡Muy importante! Esto hace que elTextdel placeholder sea “invisible” a los toques, para que el usuario pueda tocar esa área y activar elTextEditorque está debajo..frame(...)y.overlay(...): ComoTextEditorno tiene estilo, le damos un marco y un borde nosotros mismos para que parezca un campo de entrada.
Personalización y Estilo del TextEditor
A diferencia de TextField, TextEditor no tiene un .textFieldStyle(). Tienes que aplicarle los modificadores directamente.
Cambiando el fondo (iOS 16+): A partir de iOS 16, SwiftUI nos dio una forma limpia de cambiar el fondo de las vistas basadas en UIScrollView (como TextEditor).
TextEditor(text: $notesBody)
.scrollContentBackground(.hidden) // Oculta el fondo blanco por defecto
.background(Color.cyan.opacity(0.1)) // Aplica tu propio fondo
.cornerRadius(10)Cambiando el fondo (iOS 14/15): Antes de iOS 16, esto era un conocido “truco”. Tenías que desactivar el fondo de UITextView globalmente.
// Coloca esto en tu @main App struct o en un .onAppear
UITextView.appearance().backgroundColor = .clear
// ... luego en tu vista ...
TextEditor(text: $notesBody)
.background(Color.cyan.opacity(0.1)) // Ahora esto funciona
.cornerRadius(10)Esto es menos ideal porque es global, pero era la única forma. Si solo necesitas soportar iOS 16+, usa .scrollContentBackground(.hidden).
Tópicos Avanzados y Recetas Comunes
Ahora que dominas lo básico, veamos algunos problemas comunes y cómo resolverlos.
1. Manejando el Foco y Ocultando el Teclado
Este es un problema clásico, especialmente con TextEditor. El usuario termina de escribir, pero el teclado no se oculta. No hay un botón “Intro” que lo descarte.
La solución es usar @FocusState.
@FocusState nos permite vincular una variable booleana al estado de foco de un campo de texto. Luego, podemos agregar un botón a la barra de herramientas (.toolbar) para establecer ese booleano en false, lo que descarta el teclado.
struct DismissingKeyboardView: View {
@State private var notesBody: String = ""
// 1. Define un estado de foco
@FocusState private var isEditorFocused: Bool
var body: some View {
NavigationStack {
VStack {
TextEditor(text: $notesBody)
.padding()
// 2. Vincula el estado de foco al editor
.focused($isEditorFocused)
}
.navigationTitle("Editor con Foco")
.toolbar {
// 3. Agrega un botón a la barra de herramientas
ToolbarItemGroup(placement: .keyboard) {
Spacer() // Empuja el botón a la derecha
Button("Hecho") {
isEditorFocused = false // 4. Al tocarlo, quita el foco
}
}
}
}
}
}Este patrón es esencial para una buena experiencia de usuario. El ToolbarItemGroup(placement: .keyboard) coloca el botón “Hecho” justo encima del teclado, donde el usuario espera encontrarlo.
2. Limitando el Número de Caracteres
A menudo querrás limitar la entrada del usuario (por ejemplo, a 280 caracteres para un “tweet”). SwiftUI no tiene un modificador .characterLimit() incorporado, pero podemos construirlo fácilmente con .onChange().
Vigilamos la variable de texto y, si supera nuestro límite, simplemente la recortamos.
struct CharacterLimitView: View {
@State private var tweetContent: String = ""
let maxChars = 280
var body: some View {
VStack(alignment: .leading) {
TextEditor(text: $tweetContent)
.frame(height: 150)
.border(Color.gray, width: 1)
.padding()
.onChange(of: tweetContent) { oldValue, newValue in
// Recorta el texto si excede el límite
if newValue.count > maxChars {
tweetContent = String(newValue.prefix(maxChars))
}
}
// Un contador de caracteres útil
Text("\(tweetContent.count) / \(maxChars)")
.foregroundColor(tweetContent.count > maxChars ? .red : .gray)
.font(.caption)
.padding(.horizontal)
}
.padding()
}
}Este código no solo limita la entrada usando .onChange y prefix(maxChars), sino que también agrega un práctico contador de caracteres que se vuelve rojo si se alcanza el límite (aunque nuestro código ya impide superarlo).
3. El TextEditor dentro de un ScrollView o Form
¡Cuidado! Un TextEditor es, en sí mismo, un ScrollView. Poner un ScrollView dentro de otro ScrollView (como un Form o un ScrollView vertical) causa un comportamiento de desplazamiento ambiguo y, por lo general, un diseño roto.
El TextEditor intentará colapsar su altura al mínimo.
La Solución: Debes darle al TextEditor una altura fija o un rango de altura usando .frame(). Esto le dice al TextEditor “tú mides X de alto y te encargas de tu propio desplazamiento interno”, mientras que el ScrollView padre se encarga del desplazamiento de toda la página.
struct FormWithTextEditor: View {
@State private var title: String = ""
@State private var articleBody: String = ""
var body: some View {
Form {
Section(header: Text("Detalles del Artículo")) {
TextField("Título", text: $title)
}
Section(header: Text("Contenido")) {
// ¡NO HACER!
// TextEditor(text: $articleBody) // Esto colapsará
// ¡HACER!
TextEditor(text: $articleBody)
.frame(minHeight: 200) // Dale una altura mínima
}
Button("Publicar") {
// ...
}
}
}
}Conclusión
La entrada de texto multilínea en SwiftUI es potente una vez que conoces las herramientas correctas.
- Para iOS 16+ y entradas dinámicas (chats, comentarios): Usa
TextField(axis: .vertical). Es simple, soporta placeholders nativos y se integra perfectamente con.lineLimit()para controlar su crecimiento. - Para iOS 14+ o editores de texto grandes (notas, biografías): Usa
TextEditor. Recuerda que tendrás que construir tu propio placeholder usando unZStacky manejar su estilo (fondo y bordes) manualmente. No olvides darle un.frame()de altura fija cuando esté dentro de unFormoScrollView.
Ambos se benefician enormemente del uso de @FocusState y un botón “Hecho” en la barra de herramientas del teclado para una experiencia de usuario pulida. Con estas técnicas, puedes manejar cualquier requisito de entrada de texto multilínea que tus diseños exijan.
Si tienes cualquier duda sobre este artículo, contacta conmigo y estaré encantado de ayudarte 🙂. Puedes contactar conmigo en mi perfil de X o en mi perfil de Instagram








