Para que un campo numérico siga siendo editable después de que su valor opcional pase a nil, conserva el texto editable por separado del número. Primero comprueba el análisis, el estado deshabilitado y el foco; después usa un borrador respaldado por una cadena cuando tu app necesite un comportamiento de recuperación explícito.
Esta guía trata sobre un campo numérico dentro de una app de macOS que desarrollas: introducir un Tile count sintético, borrarlo e introducir otro número. El objetivo es conservar un valor vacío significativo y permitir la siguiente edición.
¿Por qué el número opcional pasó a nil?
Apple proporciona un inicializador de SwiftUI específicamente para valores opcionales: TextField(value:format:prompt:label:). Según su comportamiento documentado, el binding pasa a nil cuando el estilo de formato no puede analizar la entrada. Por tanto, una conversión fallida es un estado previsto de esta API. Apple no describe ese estado como una deshabilitación del campo. Este inicializador de estilo de formato opcional requiere macOS 12 o posterior. Inicializador de TextField de Apple para valores opcionales
Comprueba qué inicializador usa realmente tu app. El inicializador antiguo formatter: tiene un comportamiento documentado distinto ante errores: si falla la conversión, deja intacto el valor enlazado. Sustituir formatter: por format: cambia ese contrato, así que considéralo un cambio de comportamiento deliberado, no una solución universal. Inicializador de TextField de Apple basado en formatter
El problema de diseño subyacente es que un número opcional no puede representar todos los estados de edición. Un campo en blanco, un signo menos escrito parcialmente y una cadena no válida pueden carecer de un entero utilizable, pero son borradores de texto distintos. Por eso resulta útil tener un borrador separado: el campo puede conservar exactamente lo que la persona está editando mientras la app decide cuándo aceptar un valor numérico.
Diagnostica el fallo antes de cambiar el campo
Sigue esta secuencia en la ventana de tu propia app. Permite distinguir un problema de conversión de uno de interacción sin presuponer un defecto concreto de la implementación de macOS.
- Comprueba si el texto cambia realmente. Haz clic en el campo, selecciona su contenido y escribe
24. Si los caracteres cambian pero el modelo numérico no, revisa la conversión y el manejo de confirmación. - Comprueba las condiciones de interacción. Busca condiciones como
.disabled(tileCount == nil)en el campo y sus contenedores. Esa condición impide la edición necesaria para recuperarse de un valor vacío. - Comprueba si el campo sigue existiendo. Si tu vista crea el editor solo dentro de una rama
if let, establecer el valor ennilpuede quitar el editor. Mantén el borrador y el editor disponibles independientemente del número opcional. - Comprueba el foco por separado. Si el campo acepta un clic y después permite escribir, restaura el foco mediante la API pública adecuada.
- Reduce el ejemplo. Quita temporalmente las transformaciones de bindings personalizadas y la sincronización del modelo. Reproduce la transición de
12a vacío y luego a24en una vista pequeña.
Un antecesor deshabilitado importa: Apple documenta que .disabled(true) de un contenedor prevalece sobre .disabled(false) de un elemento secundario. Corrige la condición del antecesor en vez de habilitar repetidamente solo el campo. Documentación de Apple sobre disabled(_:)
Revisa también los setters de bindings personalizados. Un setter que retorna anticipadamente siempre que el número existente sea nil no puede aceptar un número de reemplazo. Del mismo modo, convertir cualquier fallo de análisis en 0 elimina la distinción entre un valor ausente y un cero real. Son decisiones de lógica de la app; cambiar el foco no las corrige.
Conserva un borrador editable y confirma el número explícitamente
Un campo respaldado por una cadena te da control sobre tres estados distintos: el texto actual, el número opcional aceptado y un mensaje de validación. Apple documenta que un TextField enlazado a una cadena actualiza continuamente su binding durante la edición. Por eso es adecuado para conservar una entrada sin terminar. Descripción general de TextField de Apple
El ejemplo completo de abajo aplica esta política: una entrada en blanco significa nil; un entero válido en base 10 reemplaza el número aceptado; una entrada no válida permanece visible y deja intacto el número aceptado. El botón Clear vacía ambos estados y solicita el foco para poder introducir otro número.
Crea en Xcode un proyecto de app SwiftUI para macOS con un destino de implementación de macOS 12 o posterior. Sustituye las declaraciones generadas de la app y de la vista de contenido por este único archivo, conservando una sola declaración @main en el destino. El ejemplo usa únicamente su propia ventana y etiquetas sintéticas. Swift y el SDK de macOS no estaban disponibles en el entorno de investigación, así que se inspeccionó este programa, pero no se compiló ni se ejecutó.
import SwiftUI
@main
struct NumericEditingDemoApp: App {
var body: some Scene {
WindowGroup {
NumericEditingView()
}
}
}
struct NumericEditingView: View {
@State private var tileCount: Int? = 12
@State private var draft = "12"
@State private var message: String? = nil
@FocusState private var fieldIsFocused: Bool
private var acceptedDescription: String {
if let value = tileCount {
return String(value)
}
return "None"
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
TextField("Tile count", text: $draft)
.textFieldStyle(.roundedBorder)
.focused($fieldIsFocused)
.onSubmit {
applyDraft()
}
HStack {
Button("Apply") {
applyDraft()
}
Button("Clear") {
tileCount = nil
draft = ""
message = nil
fieldIsFocused = true
}
Button("Edit") {
fieldIsFocused = true
}
}
Text("Accepted count: \(acceptedDescription)")
if let message = message {
Text(message)
.foregroundColor(.secondary)
}
}
.padding(20)
.frame(width: 380)
}
private func applyDraft() {
if draft.isEmpty {
tileCount = nil
message = nil
return
}
guard let parsed = Int(draft, radix: 10) else {
message = "Enter a whole number without spaces, or clear the field."
fieldIsFocused = true
return
}
tileCount = parsed
draft = String(parsed)
message = nil
}
}
El campo siempre está enlazado a draft, así que establecer tileCount en nil no elimina su estado de texto editable. La conversión solo se realiza cuando la persona hace clic en Apply o pulsa Return. Después de enviar una entrada no válida, el mensaje permanece hasta el siguiente envío o hasta pulsar Clear; el borrador sigue disponible para corregirlo.
guard evita que una conversión fallida reemplace el número aceptado. Los envíos correctos normalizan la escritura mediante String(parsed): por ejemplo, introducir +024 da como resultado 24. Esta es una política explícita del ejemplo. Elimina esa asignación de normalización si te interesa conservar la escritura enviada.
Elige deliberadamente la gramática numérica
Este ejemplo acepta enteros con signo, sin agrupación y en base 10. Rechaza espacios, fracciones decimales y separadores de agrupación. Swift documenta que Int(_:radix:) devuelve nil para una entrada no válida o un valor que el tipo entero no puede representar, por lo que un número pegado demasiado grande también sigue la ruta de validación. Inicializador de cadena de Int de Swift
Para un campo que requiera notación numérica localizada, conserva la misma estructura de borrador y confirmación, pero sustituye la conversión por una estrategia de análisis adecuada de Foundation. Un ParseableFormatStyle expone un parseStrategy, y el método parse(_:) de una estrategia realiza la conversión y puede generar un error. Gestiona el fallo conservando el borrador. Elige convenciones de formato y análisis compatibles, y luego prueba las configuraciones regionales previstas. Documentación de Apple sobre ParseableFormatStyle, Documentación de Apple sobre ParseStrategy
Si otra parte de tu app puede reemplazar el número aceptado, define cómo afecta esa actualización a un borrador sin terminar. Por ejemplo, una acción deliberada Load value puede actualizar ambos estados. Evita reconstruir continuamente el borrador a partir del número aceptado durante la edición: eso descartaría el texto sin terminar que este diseño conserva.
Restaura el foco mediante SwiftUI o AppKit
En SwiftUI, añade .focused($fieldIsFocused) al editor y establece fieldIsFocused = true desde una acción local, como hace el ejemplo. Apple documenta que asignar un valor de estado de foco mueve el foco a la vista asociada. Asigna bindings de foco distintos a los campos separados; asignar el mismo valor de foco a varios campos crea ambigüedad. Documentación de Apple sobre FocusState
El foco controla dónde continúa la edición. No cambia el analizador, no vuelve a habilitar un antecesor deshabilitado ni recrea un editor eliminado por la lógica condicional de la vista. Resuelve esas condiciones antes de añadir la recuperación del foco.
Para un campo AppKit que pertenezca a tu app, inspecciona NSTextField.isEditable. Apple define esta propiedad como el control de si el usuario puede editar el valor del campo. Restablécela a true si tu propio código la estableció incorrectamente en false, conservando las restricciones intencionadas de la app. Documentación de Apple sobre NSTextField.isEditable
Al mover explícitamente el foco en AppKit, comprueba acceptsFirstResponder y llama a makeFirstResponder(_:) de la ventana propietaria. El resultado requiere cuidado: false puede significar que el respondedor actual se negó a ceder el foco, mientras que true también puede ocurrir cuando el respondedor propuesto se niega y la ventana pasa a ser el primer respondedor. Verifica el estado de edición resultante. Para seleccionar todo el campo y reemplazar su contenido, selectText(_:) es la operación documentada; no tiene efecto antes de que el campo pertenezca a la jerarquía de vistas de una ventana. Documentación de Apple sobre makeFirstResponder(_:), Documentación de Apple sobre selectText(_:)
Respeta el ciclo de vida del editor de campo
AppKit usa currentEditor() para devolver el editor de campo actual del control, o nil si no tiene ninguno. Apple describe ese editor como un NSTextView compartido entre los controles de una ventana y creado cuando se necesita. Por consiguiente, que haya un editor nil fuera de una sesión de edición activa no demuestra que la entrada numérica esté rota. Documentación de Apple sobre currentEditor()
En el nivel del sistema de texto, un editor de campo interpreta Tab, Shift-Tab y Return como señales para terminar la edición y, posiblemente, cambiar el primer respondedor. No cambies isFieldEditor como solución a un problema de conversión numérica: esa propiedad controla el comportamiento de edición, no el análisis de números opcionales. Limita estas comprobaciones de AppKit y TextKit a los controles que pertenecen a tu app. Documentación de Apple sobre NSTextView.isFieldEditor
Verifica que tanto el borrado como la reentrada funcionen
Ejecuta esta secuencia manual con el ejemplo completo en cada versión de macOS que admita tu app. Estos son resultados esperados derivados de la lógica del ejemplo, no ejecuciones de pruebas reportadas.
| Acción | Valor aceptado esperado | Comportamiento de edición esperado |
|---|---|---|
| Iniciar | 12 |
El borrador contiene 12 |
Hacer clic en Clear |
nil, mostrado como None |
El borrador queda vacío; se solicita el foco |
Escribir 24 y luego hacer clic en Apply |
24 |
El borrador contiene 24 |
Reemplazarlo por - y luego pulsar Return |
Valor aceptado anterior | El borrador sigue siendo -; aparece un mensaje |
Reemplazarlo por -8 y luego pulsar Return |
-8 |
El mensaje desaparece |
Introducir 0 y luego hacer clic en Apply |
0 |
El cero sigue siendo distinto de la ausencia de valor |
Introducir 2.5 y luego hacer clic en Apply |
Valor aceptado anterior | El texto fraccionario permanece para corregirlo |
Vaciar el campo y luego hacer clic en Apply |
nil |
El campo sigue disponible para otra edición |
Prueba también pegar un entero mayor que Int.max, hacer clic fuera y volver al campo, y borrar el campo repetidamente. En una app integrada, incluye cualquier acción que cambie el modelo en otra parte. Estas comprobaciones evalúan por separado los fallos de conversión, la recuperación del foco y la sincronización del borrador.
La condición de recuperación útil es concreta: después de que el número aceptado pase a nil, el editor sigue presente e interactivo, y un envío válido posterior proporciona un número nuevo. Mantener el borrador independiente hace explícito ese comportamiento sin depender de cambios no documentados en los controles internos de SwiftUI.
Fuentes
- init(value:format:prompt:label:) — Apple
- init(_:value:formatter:) — Apple
- TextField — Apple
- disabled(_:) — Apple
- init(_:radix:) — Apple, biblioteca estándar de Swift
- ParseableFormatStyle — Apple
- ParseStrategy — Apple
- FocusState — Apple
- NSTextField.isEditable — Apple
- makeFirstResponder(_:) — Apple
- selectText(_:) — Apple
- currentEditor() — Apple
- NSTextView.isFieldEditor — Apple