Para insertar una sugerencia sin aplicar el valor del campo, mantén separados el autocompletado y el envío. En macOS 15 o posterior, SwiftUI ofrece sugerencias de texto nativas; usa submitScope() para impedir que se activen los controladores de envío de las vistas antecesoras y un botón explícito para la acción final.
En este tutorial crearás un pequeño selector de etiquetas dentro de tu propia app para Mac. Al escribir Bl, se ofrecen etiquetas ficticias como Blue Tile. Al elegir una sugerencia, se actualiza el borrador editable; al hacer clic en Apply Label, ese borrador se copia al valor aplicado. La distinción importa: impedir una acción de la app y consumir un comando de la tecla Retorno son requisitos diferentes.
Elige la API que se ajuste a la interacción
El modificador textInputSuggestions(_:) de Apple presenta opciones para un campo de texto. Cada sugerencia que permita una acción necesita textInputCompletion(_:), que proporciona la cadena de reemplazo. Ambas API están documentadas para macOS 15 y versiones posteriores. Una sugerencia sin un modificador de autocompletado puede aparecer, pero no responde a los clics. Apple: textInputSuggestions(_:), Apple: textInputCompletion(_:)
Para un campo de etiquetas breve, es adecuado reemplazar todo el borrador. La documentación de Apple sobre sugerencias describe este reemplazo completo del campo. No des por sentado que esta API proporciona un editor de tokens relativo al cursor ni una vista previa de un sufijo insertado en línea; esas funciones requieren un diseño de edición más específico. Apple: textInputSuggestions(_:)
| Tu requisito | Punto de partida adecuado |
|---|---|
| Ofrecer etiquetas completas en macOS 15 o posterior | Sugerencias de texto nativas de SwiftUI |
| Impedir que el campo invoque el controlador de envío de una vista antecesora | submitScope() en el campo |
| Usar un diseño de sugerencias personalizado o admitir una versión de destino anterior | Botones de sugerencia de la app que actualicen el enlace del borrador |
| Hacer que Retorno acepte un elemento resaltado y consuma ese comando | Un control de AppKit propio con gestión de comandos |
| Mostrar una vista previa de un sufijo o reemplazar la palabra en el punto de inserción | Autocompletado de texto de AppKit y un intervalo de autocompletado explícito |
Las dos últimas filas son decisiones de arquitectura derivadas de las API documentadas para comandos y autocompletado. Son útiles cuando la interacción requiere más control del que ofrecen una cadena de reemplazo y un límite de envío.
Crea un selector de etiquetas completo en SwiftUI
Crea en Xcode un proyecto de app para macOS con SwiftUI y un SDK que incluya las API de macOS 15. Establece macOS 15 o posterior como versión de destino. Reemplaza las declaraciones generadas de la app y la vista de contenido por el siguiente archivo fuente único, y deja una sola declaración @main en el destino.
El ejemplo mantiene separados draft y appliedLabel. Su regla local de coincidencia es deliberadamente sencilla: búsqueda de prefijos que distingue entre mayúsculas y minúsculas, sin sugerencias cuando el texto está vacío y sin sugerencia para una coincidencia exacta. Swift documenta hasPrefix(_:) como una comparación de prefijos de cadenas; las reglas de búsqueda más amplias dependerían de la app. Swift: Strings and Characters
import SwiftUI
@main
struct LabelCompletionApp: App {
var body: some Scene {
WindowGroup {
LabelCompletionView()
}
}
}
struct LabelCompletionView: View {
private let labels = [
"Blue Tile",
"Blue Tray",
"Green Tile",
"Round Tray"
]
@State private var draft = ""
@State private var appliedLabel = ""
@State private var applyCount = 0
@FocusState private var fieldIsFocused: Bool
private var matches: [String] {
guard !draft.isEmpty else { return [] }
return labels.filter {
$0.hasPrefix(draft) && $0 != draft
}
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Choose a label")
.font(.headline)
TextField("Type Bl to see suggestions", text: $draft)
.textFieldStyle(.roundedBorder)
.focused($fieldIsFocused)
.textInputSuggestions {
ForEach(matches, id: \.self) { label in
Text(label)
.textInputCompletion(label)
}
}
.submitScope()
Button("Apply Label") {
appliedLabel = draft
applyCount += 1
}
.disabled(draft.isEmpty)
Text("Draft: \(draft)")
Text("Applied: \(appliedLabel)")
Text("Apply count: \(applyCount)")
}
.padding(24)
.frame(width: 420)
.onAppear {
fieldIsFocused = true
}
}
}
Este es un ejemplo de app completo, que incluye sus etiquetas de muestra y su estado. Se contrastó con la documentación de API consultada, pero aquí no se compiló ni se ejecutó porque no había una cadena de herramientas de Swift ni un entorno de ejecución de la interfaz de macOS disponibles.
Por qué elegir una sugerencia no aplica la etiqueta
Cada opción de autocompletado proporciona una cadena para el campo de texto, cuyo enlace apunta a draft. El único código que asigna appliedLabel o incrementa applyCount está en la acción del botón Apply Label. Por lo tanto, en este ejemplo actualizar el borrador no activa esa acción final.
El campo también tiene submitScope(). Apple documenta que este modificador impide que los desencadenadores de envío lleguen a los modificadores de envío que están más arriba en la jerarquía de vistas. Esto importa cuando el componente se integra más adelante en un formulario más amplio con un onSubmit antecesor. Apple: submitScope(_:)
El foco inicial se establece mediante @FocusState. Apple documenta que asignar un valor a su valor enlazado mueve el foco a la vista asociada. Aquí el foco facilita la edición; no determina si se aplica la etiqueta. Apple: FocusState
Qué permite intencionadamente el ejemplo
La lista de etiquetas ofrece sugerencias, pero no restringe las entradas válidas. Al escribir Square Tile, el botón Apply Label sigue habilitado. Si tu interfaz solo permite las etiquetas de la lista, cambia explícitamente la regla de validación; por ejemplo, habilita el botón solo cuando el borrador coincida con una entrada del catálogo. No reemplaces silenciosamente un borrador que no coincida con la primera sugerencia.
El ejemplo también reemplaza la etiqueta completa. Si el borrador contiene varias palabras, elegir una opción de autocompletado debe entenderse como elegir el valor final del campo. Un editor de tokens que deba preservar el texto circundante necesita un reemplazo que tenga en cuenta la selección.
Entiende qué significa «sin enviar»
La documentación de SwiftUI para onSubmit(of:_:) indica que un campo de texto activa su acción de envío cuando el usuario pulsa Retorno. Las acciones de envío se pueden asociar a una vista individual o a una jerarquía más amplia. Inspecciona ambos lugares si parece que una selección de autocompletado activa una acción inesperada de la app. Apple: onSubmit(of:_:)
submitScope() establece un límite para los controladores antecesores. No garantiza que no se produzca un evento de envío interno, que la edición continúe activa ni que se consuma cualquier comando de teclado. De igual modo, los modificadores de autocompletado de Apple documentan el reemplazo de texto, no la supresión universal del comportamiento de la tecla Retorno. Ten presentes estos límites al elegir la interacción. Apple: submitScope(_:), Apple: textInputCompletion(_:)
En un componente existente, inspecciona estas rutas por separado:
- Quita cualquier acción final de la app del
onSubmitdel propio campo si Retorno nunca debe aplicar su valor. - Coloca
submitScope()en el campo para bloquear una acción de envío antecesora. - Comprueba por separado los atajos de teclado de los botones, especialmente si hay una acción predeterminada explícita.
- Comprueba si algún observador de cambios o setter del modelo realiza la acción final cada vez que cambia el borrador.
Una clausura local onSubmit vacía no es el reemplazo documentado para un límite de envío. Apple proporciona específicamente submitScope() para detener la propagación. Si la app debe enviarse al pulsar Retorno solo cuando no hay ningún autocompletado activo, esa política condicional de teclado requiere estado y gestión de comandos adicionales. Apple: onSubmit(of:_:)
Usa AppKit cuando Retorno deba aceptar y detenerse
Para un NSTextField propio, AppKit proporciona control(_:textView:doCommandBy:). Su argumento de vista de texto es el editor de campo del control. El delegado puede gestionar un comando asociado y devolver true; devolver false indica que el delegado no lo gestionó. Este es el punto de extensión documentado para personalizar comandos en un campo de texto. Apple: control(_:textView:doCommandBy:)
Un controlador de autocompletado personalizado puede usar ese punto de extensión con esta secuencia:
- Lleva un registro de si la lista de sugerencias está abierta y de qué elemento está resaltado.
- Cuando llega el comando Retorno y hay un elemento resaltado válido, inserta ese elemento en el borrador.
- Cierra la lista de sugerencias y sincroniza el modelo del borrador de la app.
- Devuelve
trueporque el controlador gestionó el comando. - Devuelve
falsepara los comandos que el controlador no gestiona.
Esta secuencia es un diseño de implementación derivado del contrato del delegado, no una política de autocompletado integrada. Decide por separado qué debe hacer Retorno cuando la lista está cerrada. Si en cambio tienes un NSTextView independiente, el punto de conexión correspondiente es textView(_:doCommandBy:); Apple indica que devolver true impide que la vista de texto ejecute ese comando por sí misma. Apple: textView(_:doCommandBy:)
Para el autocompletado del texto, NSTextView.insertCompletion(_:forPartialWordRange:movement:isFinal:) admite cambios de reemplazo y selección. Con isFinal: false, selecciona el sufijo propuesto; con isFinal: true, contrae la selección después de la palabra completada. La opción describe el estado del autocompletado, no el envío de la app. El argumento movement identifica cómo se realizó la navegación o selección del autocompletado. Apple: insertCompletion(_:forPartialWordRange:movement:isFinal:)
Conserva el intervalo de texto correcto
rangeForUserCompletion de AppKit identifica la palabra parcial desde su inicio hasta el punto de inserción. Usa un intervalo adecuado para el reemplazo previsto y vuelve a calcularlo después de editar o mover el cursor. No reutilices a ciegas un intervalo capturado con anterioridad después de que cambie el texto. Apple: rangeForUserCompletion
Al convertir entre intervalos de texto, no sustituyas la longitud de NSString por el recuento de caracteres de Swift. La documentación de Swift explica que String cuenta clústeres de grafemas extendidos, mientras que la longitud de NSString cuenta unidades de código UTF-16. Esos valores pueden diferir. El reemplazo de todo el campo mediante el enlace del ejemplo de SwiftUI evita por completo esta conversión de intervalos. Swift: Strings and Characters)
Verifica la inserción y la aplicación por separado
Usa el borrador visible, el valor aplicado y el contador para comprobar el resultado en cada versión de macOS compatible. Lo siguiente son comprobaciones previstas para el ejemplo, no resultados de pruebas registrados.
| Acción | Resultado observable previsto |
|---|---|
Escribir Bl |
El borrador es Bl; se ofrecen las etiquetas coincidentes |
Hacer clic en Blue Tile |
El borrador pasa a ser Blue Tile; el valor aplicado y el contador no cambian |
| Pulsar Retorno durante la edición | La app no aplica el borrador |
Hacer clic en Apply Label |
El valor aplicado copia el borrador; el contador aumenta una vez |
| Editar el borrador después | El valor aplicado anterior sigue visible |
| Borrar el borrador | La lista de sugerencias queda vacía y Apply Label se deshabilita |
Escribir bl |
No hay coincidencias con la regla del ejemplo, que distingue entre mayúsculas y minúsculas |
Prueba también la selección con el teclado en la lista nativa de sugerencias y repite las comprobaciones después de integrar el campo en su vista antecesora real. Apple señala que eventos como mover una ventana de macOS pueden cerrar la interfaz nativa de sugerencias, por lo que el borrador debe seguir siendo utilizable aunque la lista no esté. Apple: textInputSuggestions(_:)
Si la versión de destino es anterior a macOS 15, ofrece una vista nativa con disponibilidad condicionada y una alternativa, o usa botones de sugerencia de la app en todos los casos. Un botón alternativo puede asignar directamente su etiqueta al borrador y dejar la acción final en Apply Label. Así se mantiene la misma separación entre borrador y aplicación; la navegación por teclado y la presentación quedan a cargo de tu componente.
Para este selector de etiquetas, el límite útil se puede observar: aceptar una sugerencia de autocompletado cambia el borrador editable, y solo la acción final explícita cambia la etiqueta aplicada. Usa las sugerencias nativas y el ámbito de envío de SwiftUI para establecer ese límite; pasa a la gestión de comandos de AppKit cuando el requisito incluya consumir la propia tecla Retorno.
Fuentes
- textInputSuggestions(_:) — Apple
- textInputCompletion(_:) — Apple
- submitScope(_:) — Apple
- onSubmit(of:_:) — Apple
- FocusState — Apple
- control(_:textView:doCommandBy:) — Apple
- textView(_:doCommandBy:) — Apple
- insertCompletion(_:forPartialWordRange:movement:isFinal:) — Apple
- rangeForUserCompletion — Apple
- Strings and Characters — Swift.org