Pour qu’un champ numérique reste modifiable après que sa valeur facultative est devenue nil, conservez séparément le texte modifiable et le nombre. Vérifiez d’abord l’analyse, l’état désactivé et le focus ; utilisez ensuite un brouillon stocké sous forme de chaîne si votre app doit gérer explicitement la récupération.
Ce guide porte sur un champ numérique dans une app macOS que vous développez : saisir un Tile count fictif, l’effacer, puis saisir un autre nombre. L’objectif est de conserver une valeur vide significative tout en permettant la saisie suivante.
Pourquoi le nombre facultatif est-il devenu nil ?
Apple fournit un initialiseur SwiftUI conçu spécifiquement pour les valeurs facultatives : TextField(value:format:prompt:label:). Son comportement documenté consiste à définir la liaison sur nil lorsque le style de format ne parvient pas à analyser la saisie. Une conversion échouée est donc un état attendu de cette API. Apple ne décrit pas cet état comme désactivant le champ. Cet initialiseur facultatif avec style de format nécessite macOS 12 ou une version ultérieure. Initialiseur TextField d’Apple pour les valeurs facultatives
Vérifiez quel initialiseur votre app utilise réellement. L’ancien initialiseur formatter: a un comportement documenté différent en cas d’échec : si la conversion échoue, il laisse la valeur liée inchangée. Remplacer formatter: par format: modifie ce contrat ; considérez donc cela comme un changement de comportement délibéré, et non comme une correction universelle. Initialiseur TextField d’Apple basé sur un formateur
Le problème de conception sous-jacent est qu’un nombre facultatif ne peut pas représenter tous les états de saisie. Un champ vide, un signe moins saisi partiellement et une chaîne non valide peuvent tous être dépourvus d’un entier exploitable, mais correspondent à des brouillons de texte différents. C’est pourquoi un brouillon distinct est utile : le champ peut conserver exactement le texte en cours de saisie, tandis que l’app décide quand accepter une valeur numérique.
Diagnostiquer le problème avant de modifier le champ
Suivez les étapes ci-dessous dans la fenêtre de votre app. Elles permettent de distinguer un problème de conversion d’un problème d’interaction sans présumer d’un défaut particulier de l’implémentation de macOS.
- Vérifiez si le texte change réellement. Cliquez dans le champ, sélectionnez son contenu et saisissez
24. Si les caractères changent, mais pas le modèle numérique, examinez la conversion et la validation de la saisie. - Vérifiez les conditions d’interaction. Recherchez dans le champ et ses conteneurs des conditions telles que
.disabled(tileCount == nil). Cette condition empêche précisément la modification nécessaire pour récupérer d’une valeur vide. - Vérifiez si le champ existe toujours. Si votre vue crée l’éditeur uniquement dans une branche
if let, définir la valeur surnilpeut supprimer l’éditeur. Gardez son brouillon et l’éditeur disponibles indépendamment du nombre facultatif. - Vérifiez le focus séparément. Si le champ accepte un clic et que la saisie fonctionne ensuite, rétablissez le focus au moyen de l’API publique appropriée.
- Réduisez l’exemple. Retirez temporairement les transformations personnalisées de liaison et la synchronisation du modèle. Reproduisez dans une petite vue la transition de
12à vide, puis à24.
Un conteneur désactivé compte : Apple précise que le .disabled(true) d’un conteneur prévaut sur celui d’un élément enfant, .disabled(false). Corrigez la condition du conteneur au lieu de réactiver sans cesse uniquement le champ. Documentation Apple de disabled(_:)
Examinez également les accesseurs de liaison personnalisés. Un accesseur qui retourne immédiatement dès que le nombre actuel est nil ne peut pas accepter un nombre de remplacement. De même, convertir tout échec d’analyse en 0 fait perdre la distinction entre une valeur absente et un zéro réel. Ce sont des décisions de logique applicative ; modifier le focus ne peut pas les corriger.
Conserver un brouillon modifiable et valider explicitement le nombre
Un champ stocké sous forme de chaîne vous permet de contrôler trois états distincts : le texte actuel, le nombre facultatif accepté et un message de validation. Apple précise qu’un TextField lié à une chaîne met à jour sa liaison en continu pendant la saisie. Il convient donc pour conserver une saisie inachevée. Présentation Apple de TextField
L’exemple complet ci-dessous applique la règle suivante : une saisie vide signifie nil ; un entier valide en base 10 remplace le nombre accepté ; une saisie non valide reste visible et laisse le nombre accepté inchangé. Le bouton Clear vide les deux états et demande le focus afin qu’un autre nombre puisse être saisi.
Créez dans Xcode un projet d’app SwiftUI pour macOS avec une cible de déploiement macOS 12 ou ultérieure. Remplacez les déclarations générées de l’app et de la vue de contenu par ce fichier unique, en ne conservant qu’une seule déclaration @main dans la cible. L’exemple utilise uniquement sa propre fenêtre et des libellés fictifs. Swift et le SDK macOS n’étaient pas disponibles dans l’environnement de recherche ; ce programme a donc été examiné, mais ni compilé ni exécuté.
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
}
}
Le champ est toujours lié à draft ; définir tileCount sur nil ne supprime donc pas son état de texte modifiable. La conversion a lieu uniquement lorsque la personne clique sur Apply ou appuie sur Retour. Après une soumission invalide, le message reste affiché jusqu’à la soumission suivante ou à l’action Clear ; le brouillon reste disponible pour correction.
Le guard empêche une conversion échouée de remplacer le nombre accepté. Les soumissions réussies normalisent l’écriture au moyen de String(parsed) : par exemple, saisir +024 donne 24. Il s’agit d’une règle explicite propre à cet exemple. Supprimez cette affectation de normalisation si vous souhaitez conserver l’écriture saisie.
Choisir délibérément la grammaire numérique
Cet exemple accepte les entiers signés sans séparateur, en base 10. Il rejette les espaces, les fractions décimales et les séparateurs de groupes. Swift précise que Int(_:radix:) renvoie nil pour une saisie invalide ou une valeur que le type entier ne peut pas représenter ; un nombre collé trop grand suit donc également le parcours de validation. Initialiseur de chaîne Int de Swift
Pour un champ qui nécessite une notation numérique localisée, conservez la même structure avec brouillon et validation, mais remplacez la conversion par une stratégie d’analyse Foundation appropriée. Un ParseableFormatStyle expose un parseStrategy, et la méthode parse(_:) d’une stratégie effectue la conversion et peut lever une erreur. Gérez l’échec tout en conservant le brouillon. Choisissez des conventions de formatage et d’analyse compatibles, puis testez les paramètres régionaux visés. Documentation Apple de ParseableFormatStyle, Documentation Apple de ParseStrategy
Si une autre partie de votre app peut remplacer le nombre accepté, définissez l’effet de cette mise à jour sur un brouillon inachevé. Par exemple, une action délibérée Load value peut mettre à jour les deux états. Évitez de recréer continuellement le brouillon à partir du nombre accepté pendant la saisie : cela supprimerait le texte inachevé que cette conception préserve.
Rétablir le focus avec SwiftUI ou AppKit
Dans SwiftUI, associez .focused($fieldIsFocused) à l’éditeur et définissez fieldIsFocused = true à partir d’une action locale, comme dans l’exemple. Apple précise que l’affectation d’une valeur d’état de focus déplace le focus vers la vue associée. Attribuez des liaisons de focus distinctes aux champs séparés ; attribuer la même valeur de focus à plusieurs champs crée une ambiguïté. Documentation Apple de FocusState
Le focus détermine où la saisie a lieu. Il ne modifie pas l’analyseur, ne réactive pas un conteneur désactivé et ne recrée pas un éditeur supprimé par la logique conditionnelle de la vue. Résolvez ces problèmes avant d’ajouter la récupération du focus.
Pour un champ AppKit que votre app possède, examinez NSTextField.isEditable. Apple définit cette propriété comme contrôlant si l’utilisateur peut modifier la valeur du champ. Rétablissez-la à true si votre code l’a mise à faux par erreur, tout en préservant les restrictions intentionnelles de l’app. Documentation Apple de NSTextField.isEditable
Pour déplacer explicitement le focus AppKit, vérifiez acceptsFirstResponder et appelez makeFirstResponder(_:) sur la fenêtre propriétaire. Le résultat demande de l’attention : false peut signifier que le répondeur actuel a refusé de céder le focus, tandis que true peut également se produire lorsque le répondeur proposé refuse et que la fenêtre devient le premier répondeur. Vérifiez l’état de saisie obtenu. Pour sélectionner tout le champ afin de le remplacer, selectText(_:) est l’opération documentée ; elle n’a aucun effet avant que le champ appartienne à la hiérarchie de vues d’une fenêtre. Documentation Apple de makeFirstResponder(_:), Documentation Apple de selectText(_:)
Respecter le cycle de vie de l’éditeur de champ
La méthode AppKit currentEditor() renvoie l’éditeur de champ actuel du contrôle, ou nil s’il n’en a pas. Apple décrit cet éditeur comme un NSTextView partagé entre les contrôles d’une fenêtre et créé à la demande. Par conséquent, un éditeur nil en dehors d’une session de saisie active ne prouve pas que la saisie numérique est défectueuse. Documentation Apple de currentEditor()
Au niveau du système de texte, un éditeur de champ traite Tab, Maj-Tab et Retour comme des signaux pour terminer la saisie et éventuellement changer le premier répondeur. Ne modifiez pas isFieldEditor pour corriger la conversion numérique : cette propriété régit le comportement de saisie, et non l’analyse des nombres facultatifs. Limitez ces vérifications AppKit et TextKit aux contrôles que votre app possède. Documentation Apple de NSTextView.isFieldEditor
Vérifier que l’effacement et la nouvelle saisie fonctionnent
Exécutez cette séquence manuelle sur l’exemple complet pour chaque version de macOS prise en charge par votre app. Il s’agit des résultats attendus d’après la logique de l’exemple, et non de tests prétendument exécutés.
| Action | Valeur acceptée attendue | Comportement de saisie attendu |
|---|---|---|
| Lancer | 12 |
Le brouillon contient 12 |
Cliquer sur Clear |
nil, affiché comme None |
Le brouillon est vide ; le focus est demandé |
Saisir 24, puis cliquer sur Apply |
24 |
Le brouillon contient 24 |
Remplacer par -, puis appuyer sur Retour |
Valeur acceptée précédente | Le brouillon reste - ; un message apparaît |
Remplacer par -8, puis appuyer sur Retour |
-8 |
Le message disparaît |
Saisir 0, puis cliquer sur Apply |
0 |
Le zéro reste distinct d’une valeur absente |
Saisir 2.5, puis cliquer sur Apply |
Valeur acceptée précédente | Le texte fractionnaire reste disponible pour correction |
Vider le champ, puis cliquer sur Apply |
nil |
Le champ reste disponible pour une nouvelle saisie |
Testez également le collage d’un entier supérieur à Int.max, le fait de cliquer ailleurs puis de revenir, ainsi que l’effacement répété du champ. Dans une app intégrée, incluez toute action qui modifie le modèle ailleurs. Ces vérifications testent séparément l’échec de conversion, la récupération du focus et la synchronisation du brouillon.
La condition de récupération utile est concrète : après que le nombre accepté devient nil, l’éditeur reste présent et interactif, et une soumission valide ultérieure fournit un nouveau nombre. Garder le brouillon indépendant rend ce comportement explicite sans dépendre de modifications non documentées des contrôles sous-jacents de SwiftUI.
Sources
- init(value:format:prompt:label:) — Apple
- init(_:value:formatter:) — Apple
- TextField — Apple
- disabled(_:) — Apple
- init(_:radix:) — Apple, bibliothèque standard Swift
- ParseableFormatStyle — Apple
- ParseStrategy — Apple
- FocusState — Apple
- NSTextField.isEditable — Apple
- makeFirstResponder(_:) — Apple
- selectText(_:) — Apple
- currentEditor() — Apple
- NSTextView.isFieldEditor — Apple