En una app para Mac con filas editables de una sola línea, gestiona Retorno con onSubmit, inserta un modelo nuevo justo debajo de la fila actual y enfoca su campo con FocusState. Asigna a cada fila un ID estable para que la inserción conserve la relación entre el texto, la fila y el foco.
Este tutorial crea un pequeño editor de listas local con entradas de ejemplo como Item A. Al pulsar Retorno, se crea una fila vacía debajo del campo que se está editando, se conserva el texto existente y el foco del teclado pasa al campo nuevo. El texto no se divide en el punto de inserción.
¿Qué control de texto admite este comportamiento?
Usa un TextField de una sola línea cuando cada fila represente una entrada breve. Apple documenta explícitamente que un TextField ejecuta su acción onSubmit cuando el usuario pulsa la tecla de retorno física o en pantalla. Apple también documenta que asignar un valor de FocusState mueve el foco a la vista asociada. Ambas API están disponibles en macOS 12 y versiones posteriores. Apple: onSubmit(of:_:), Apple: FocusState.
Elige el control según lo que edite el usuario:
| Tarea de edición | Punto de partida | Comportamiento de Retorno que se debe implementar |
|---|---|---|
| Una entrada breve por fila | TextField de una sola línea |
Enviar, insertar debajo y enfocar la fila nueva |
| Un párrafo dentro de cada fila | TextEditor o una vista de texto de AppKit |
Definir en qué se diferencia insertar una fila de insertar un salto de línea |
| Un documento con comandos de texto personalizados | NSTextView integrada en SwiftUI |
Gestionar el comando de texto correspondiente y coordinar los cambios del modelo |
Apple describe TextEditor como un editor multilínea con desplazamiento. Su documentación no establece el mismo comportamiento de envío con Retorno que sí documenta para TextField. Por tanto, cambiar el ejemplo a TextEditor requiere reconsiderar la gestión de comandos; añadir solo el mismo modificador no es un equivalente documentado. Apple: TextEditor, Apple: onSubmit(of:_:).
Mantén la identidad de la fila separada de su posición
Tres valores tienen propósitos distintos: el ID de la fila identifica la entrada, su índice en el arreglo determina dónde insertar y el ID enfocado identifica el campo que debe recibir la entrada del teclado.
El ForEach de Apple crea vistas a partir de datos identificados. Usa un modelo Identifiable con un ID que se almacene al crear la fila. En este diseño, ese mismo ID también se convierte en el valor de foco de la fila. Esa relación permite que una entrada conserve su identidad cuando se inserta otra encima. Apple: ForEach.
Evita usar el texto editable como ID: dos filas vacías tendrían la misma identidad y, al editar, esta cambiaría. Las posiciones del arreglo tampoco son buenas identidades para esta tarea de inserción, porque cambian. Estas son consecuencias del modelo de datos propuesto, no requisitos adicionales de SwiftUI.
Cada campo necesita un valor de foco distinto. Apple advierte que vincular el mismo valor de foco a varias vistas crea una vinculación de foco ambigua. Un UUID almacenado para cada fila ofrece una forma sencilla de distinguir los campos. Apple: FocusState.
Crea el ejemplo completo para macOS
Crea en Xcode un proyecto de app para macOS con SwiftUI y establece como destino de implementación macOS 12 o una versión posterior. Sustituye el punto de entrada de la app y la vista de contenido generados por el siguiente archivo único, y elimina la declaración original de la app para que el destino tenga un solo punto de entrada @main.
El ejemplo usa un VStack normal dentro de una vista desplazable. Registra la fila nueva como destino de foco pendiente y, después, solicita el foco cuando aparece ese campo. Esta estrategia de implementación se deriva de las API de foco y de aparición de Apple; Apple no documenta las llamadas de aparición como una garantía universal de que el foco esté listo. El momento exacto de onAppear depende de la vista. Apple: onAppear(perform:).
import SwiftUI
import Foundation
import Dispatch
private struct EditableRow: Identifiable {
let id = UUID()
var text: String
}
@main
struct RowEntryDemoApp: App {
var body: some Scene {
WindowGroup {
RowEditor()
}
}
}
private struct RowEditor: View {
@State private var rows = [
EditableRow(text: "Item A"),
EditableRow(text: "Item B")
]
@State private var pendingFocusID: UUID?
@FocusState private var focusedRowID: UUID?
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Local row editor")
.font(.headline)
Text("Press Return in a field to insert a row below it.")
.font(.subheadline)
ScrollViewReader { proxy in
ScrollView {
VStack(spacing: 10) {
ForEach($rows) { $row in
TextField("New item", text: $row.text)
.textFieldStyle(.roundedBorder)
.focused($focusedRowID, equals: row.id)
.onSubmit {
insertRow(after: row.id)
}
.id(row.id)
.onAppear {
focusIfPending(row.id) {
proxy.scrollTo(row.id, anchor: .center)
}
}
}
}
.padding(4)
}
}
}
.padding(20)
.frame(minWidth: 420, minHeight: 280)
}
private func insertRow(after id: UUID) {
guard pendingFocusID == nil,
let index = rows.firstIndex(where: { $0.id == id })
else { return }
let newRow = EditableRow(text: "")
pendingFocusID = newRow.id
rows.insert(newRow, at: index + 1)
}
private func focusIfPending(
_ id: UUID,
scroll: @escaping () -> Void
) {
guard pendingFocusID == id else { return }
DispatchQueue.main.async {
guard pendingFocusID == id,
rows.contains(where: { $0.id == id })
else { return }
scroll()
focusedRowID = id
pendingFocusID = nil
}
}
}
El modelo contiene todos los datos de ejemplo y el archivo define todos los tipos y funciones referenciados. En el entorno de investigación no había disponible un compilador de Swift ni un entorno de ejecución de macOS, por lo que se inspeccionó este programa, pero no se compiló ni se ejecutó.
Cómo funcionan en conjunto la inserción y el foco
La vinculación de cadena del campo actualiza la fila correspondiente mientras el usuario edita. Por tanto, el controlador de envío puede insertar una fila nueva sin reemplazar ni borrar la entrada actual. Apple documenta las actualizaciones continuas de los campos de texto vinculados a una cadena. Apple: TextField.
El controlador de inserción busca por ID la fila que se está enviando e inserta en index + 1. En una fila intermedia, así coloca la entrada nueva antes de la siguiente entrada existente. En la última fila, la posición equivale a endIndex del arreglo, un índice de inserción válido que añade el elemento al final. Apple: insert(_:at:).
La llamada de aparición del campo nuevo comprueba si su ID coincide con la solicitud pendiente. El bloque enviado a la cola vuelve a comprobarlo antes de desplazarse y asignar el foco. Estas comprobaciones impiden que una llamada de aparición no relacionada consuma la solicitud. El ejemplo también ignora nuevos envíos mientras haya una solicitud de foco pendiente.
El envío a la cola adicional difiere el trabajo para que no se ejecute dentro de la llamada de aparición; no es una barrera de renderizado documentada. Verifica esta decisión de programación en las versiones de macOS compatibles y en la jerarquía real de contenedores.
El desplazamiento y el foco son operaciones distintas. scrollTo localiza un elemento secundario mediante su identificador de vista y se desplaza hacia él; asignar focusedRowID solicita el foco del teclado. El .id(row.id) explícito proporciona el destino del desplazamiento. Apple: scrollTo(_:anchor:).
Comprueba el resultado y diagnostica los fallos habituales
Sigue esta secuencia de aceptación con las entradas de ejemplo:
- Haz clic en
Item A, escribe texto adicional y pulsaRetornouna vez. - Comprueba que aparece un campo vacío entre las dos entradas originales.
- Escribe
Item Cinmediatamente y comprueba que aparece en el campo nuevo. - Pulsa
Retornoen el último campo y comprueba que aparece una fila nueva al final. - Pulsa
Retornoen un campo vacío y comprueba que se permite otra fila vacía. - Sigue hasta que sea necesario desplazarse y, entonces, comprueba que el campo insertado quede visible y acepte texto.
Estos son los resultados esperados que debes verificar localmente. El ejemplo permite intencionadamente las entradas vacías y conserva la cadena actual completa, incluso si el punto de inserción está en medio. Si la tarea requiere dividir esa cadena en dos entradas, también tendrás que gestionar la selección y los rangos de texto.
Si aparece una fila, pero al escribir el texto sigue en el campo anterior, comprueba el ID enfocado, la llamada de aparición del campo nuevo y si la ventana está activa. Revisa también si otra llamada restablece el foco. Evita regenerar los ID de las filas al calcular la vista.
Si la inserción ocurre dos veces, revisa los controladores de envío que haya en niveles superiores de la jerarquía de vistas. Apple permite usar onSubmit en controles individuales y vistas contenedoras, y documenta submitScope como una forma de impedir que el envío se propague a controladores superiores. Deja la acción de inserción a cargo de un solo controlador. Apple: onSubmit(of:_:).
Cambiar la etiqueta de envío no implementa la navegación ni la inserción. Apple define submitLabel como la configuración de la etiqueta semántica de envío. La mutación del modelo y la asignación del foco siguen necesitando código propio. Apple: submitLabel(_:).
Si la colección es mucho mayor, reconsidera la pila que carga todas las vistas de inmediato. Apple señala que contenedores como List y LazyVStack consultan de forma diferida los elementos de ForEach. Por tanto, quizá haya que llevar un campo que esté fuera de pantalla a la vista antes de que su llamada de aparición pueda completar la solicitud de foco. No sustituyas el contenedor sin probar esa secuencia. Apple: ForEach.
¿Cuándo conviene usar AppKit y TextKit?
Usa el ejemplo de SwiftUI para entradas breves e independientes. Si las filas contienen texto multilínea o requieren un control preciso de los comandos de texto, una alternativa compatible es integrar tu propio NSTextView mediante NSViewRepresentable. Apple documenta los representables como el puente para crear y actualizar vistas de AppKit dentro de SwiftUI, con un coordinador que reenvía las interacciones del delegado. Apple: NSViewRepresentable.
Para esa alternativa, conserva el mismo modelo con ID de fila e implementa esta secuencia:
- Crea la vista de texto y asigna un coordinador como su delegado.
- Reenvía los cambios de texto a la fila correspondiente del modelo.
- Implementa
textView(_:doCommandBy:)y reconoce el comandoinsertNewline(_:)según el comportamiento que hayas elegido para el editor. - Si gestionas ese comando como inserción de fila, conserva el texto actual, inserta la fila nueva y devuelve
truepara consumir el comando. Devuelvefalsepara los comandos que el delegado no gestione. - Cuando la vista de texto nueva esté conectada a su ventana contenedora, solicita el foco mediante el método
makeFirstResponder(_:)de esa ventana.
Apple documenta tanto el comando de salto de línea como el contrato booleano del delegado: true significa que el delegado gestionó el comando, mientras que false permite que la vista de texto lo intente. Esto proporciona una vía explícita para gestionar comandos en un editor propio de la app. Apple: insertNewline(_:), Apple: textView(_:doCommandBy:).
Conserva la composición de texto activa al añadir una gestión de comandos personalizada. AppKit ofrece hasMarkedText() para indicar si hay texto marcado; usa ese estado al decidir si el delegado debe dejar que el sistema de texto gestione un comando. Prueba la composición en el editor resultante en lugar de dar por hecho que toda interacción relacionada con Retorno debe insertar una fila. Apple: hasMarkedText().
Para el foco, Apple indica explícitamente que se use makeFirstResponder(_:) de la ventana contenedora, en lugar de llamar directamente a becomeFirstResponder() de la vista de texto. El acceso al diseño de TextKit también tiene una consecuencia específica de la versión: en macOS 12 y versiones posteriores, acceder explícitamente a layoutManager de una vista de texto la cambia al modo de compatibilidad, que usa NSLayoutManager. La inserción de filas no requiere ese acceso. Apple: NSTextView.
Para la tarea de una sola línea, empieza con el ejemplo completo de SwiftUI y verifica en tu app para Mac la secuencia de inserción, visibilidad y escritura. Una identidad estable, una posición de inserción explícita y una solicitud de foco dirigida proporcionan el comportamiento esencial; pasa a una vista de texto de AppKit cuando la tarea de edición necesite controles para comandos y selección.
Fuentes
- onSubmit(of:_:) — Apple
- FocusState — Apple
- TextField — Apple
- TextEditor — Apple
- ForEach — Apple
- onAppear(perform:) — Apple
- insert(_:at:) — Apple
- scrollTo(_:anchor:) — Apple
- submitLabel(_:) — Apple
- NSViewRepresentable — Apple
- insertNewline(_:) — Apple
- textView(_:doCommandBy:) — Apple
- hasMarkedText() — Apple
- NSTextView — Apple