Para manter um campo numérico editável depois que seu valor opcional se torna nil, preserve o texto editável separadamente do número. Primeiro, verifique a análise, o estado desativado e o foco; depois, use um rascunho baseado em string quando o app precisar de um comportamento explícito de recuperação.

Este guia aborda um campo numérico em um app para macOS que você desenvolve: inserir um Tile count sintético, apagá-lo e inserir outro número. O objetivo é preservar um valor vazio significativo e permitir a próxima edição.

Por que o número opcional se tornou nil?

A Apple fornece um inicializador do SwiftUI específico para valores opcionais: TextField(value:format:prompt:label:). O comportamento documentado é definir o binding como nil quando o estilo de formato não consegue analisar a entrada. Portanto, uma conversão malsucedida é um estado esperado dessa API. A Apple não descreve esse estado como uma desativação do campo. Esse inicializador opcional com estilo de formato requer o macOS 12 ou posterior. Inicializador TextField da Apple para valores opcionais

Verifique qual inicializador seu app realmente usa. O inicializador mais antigo formatter: tem um comportamento documentado diferente em caso de falha: se a conversão falhar, ele mantém o valor vinculado inalterado. Substituir formatter: por format: altera esse contrato; trate isso como uma mudança deliberada de comportamento, não como uma correção universal. Inicializador TextField da Apple baseado em formatter

O problema de design subjacente é que um número opcional não consegue representar todos os estados de edição. Um campo em branco, um sinal de menos digitado parcialmente e uma string inválida podem não conter um inteiro utilizável, mas são rascunhos de texto diferentes. Por isso, um rascunho separado é útil: o campo pode manter exatamente o que a pessoa está editando enquanto o app decide quando aceitar um valor numérico.

Diagnostique a falha antes de alterar o campo

Siga esta sequência na janela do seu app. Ela distingue um problema de conversão de um problema de interação sem pressupor um defeito específico da implementação do macOS.

  1. Verifique se o texto realmente muda. Clique no campo, selecione o conteúdo e digite 24. Se os caracteres mudarem, mas o modelo numérico não, examine a conversão e o tratamento da confirmação.
  2. Verifique as condições de interação. Procure condições como .disabled(tileCount == nil) no campo e nos contêineres dele. Essa condição impede a própria edição necessária para se recuperar de um valor vazio.
  3. Verifique se o campo ainda existe. Se sua view criar o editor apenas dentro de uma ramificação if let, definir o valor como nil pode remover o editor. Mantenha o rascunho e o editor disponíveis independentemente do número opcional.
  4. Verifique o foco separadamente. Se o campo aceitar um clique e, em seguida, a digitação funcionar, restaure o foco pela API pública apropriada.
  5. Reduza o exemplo. Remova temporariamente as transformações personalizadas do binding e a sincronização do modelo. Reproduza a transição de 12 para vazio e depois para 24 em uma view pequena.

Um ancestral desativado é relevante: a Apple documenta que .disabled(true) de um contêiner substitui .disabled(false) em um elemento filho. Corrija a condição do ancestral em vez de reativar repetidamente apenas o campo. Documentação da Apple sobre disabled(_:)

Examine também os setters de bindings personalizados. Um setter que retorna imediatamente sempre que o número existente é nil não pode aceitar um número substituto. Da mesma forma, mapear toda conversão malsucedida para 0 elimina a distinção entre um valor ausente e um zero real. Essas são decisões da lógica do app; alterar o foco não as corrige.

Mantenha um rascunho editável e confirme o número explicitamente

Um campo baseado em string dá a você controle sobre três estados separados: o texto atual, o número opcional aceito e uma mensagem de validação. A Apple documenta que um TextField vinculado a uma string atualiza o binding continuamente durante a edição. Isso o torna adequado para reter entradas incompletas. Visão geral do TextField da Apple

O exemplo completo abaixo usa esta política: uma entrada em branco significa nil; um inteiro válido na base 10 substitui o número aceito; uma entrada inválida permanece visível e mantém o número aceito inalterado. O botão Clear apaga os dois estados e solicita o foco para que outro número possa ser inserido.

Crie um projeto de app SwiftUI para macOS no Xcode com destino de implantação no macOS 12 ou posterior. Substitua as declarações geradas do app e da view de conteúdo por este arquivo único, mantendo apenas uma declaração @main no destino. O exemplo usa somente a própria janela e rótulos sintéticos. Swift e o SDK do macOS não estavam disponíveis no ambiente de pesquisa, portanto o programa foi inspecionado, mas não compilado nem executado.

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
    }
}

O campo sempre está vinculado a draft, portanto definir tileCount como nil não remove o estado do texto editável. A conversão só acontece quando a pessoa clica em Apply ou pressiona Return. Depois de um envio inválido, a mensagem permanece até o próximo envio ou até Clear; o rascunho continua disponível para correção.

O guard impede que uma conversão malsucedida substitua o número aceito. Envios bem-sucedidos normalizam a grafia por meio de String(parsed): por exemplo, inserir +024 resulta em 24. Essa é uma política explícita do exemplo. Remova essa atribuição de normalização se for útil preservar a grafia enviada.

Escolha deliberadamente a gramática numérica

Este exemplo aceita inteiros com sinal, sem separadores, na base 10. Ele rejeita espaços, frações decimais e separadores de agrupamento. A documentação do Swift informa que Int(_:radix:) retorna nil para entradas inválidas ou valores que o tipo inteiro não consegue representar; assim, um número colado grande demais também segue o caminho de validação. Inicializador de string de Int do Swift

Para um campo que exija notação numérica localizada, mantenha a mesma estrutura de rascunho e confirmação, mas substitua a conversão por uma estratégia de análise apropriada do Foundation. Um ParseableFormatStyle expõe um parseStrategy, e o método parse(_:) de uma estratégia realiza a conversão e pode lançar um erro. Trate a falha mantendo o rascunho. Escolha convenções de formatação e análise correspondentes e teste as localidades desejadas. Documentação da Apple sobre ParseableFormatStyle, documentação da Apple sobre ParseStrategy

Se outra parte do app puder substituir o número aceito, defina como essa atualização afeta um rascunho incompleto. Por exemplo, uma ação deliberada Load value pode atualizar ambos os estados. Evite reconstruir continuamente o rascunho a partir do número aceito durante a edição: isso descartaria o texto incompleto preservado por este design.

Restaure o foco pelo SwiftUI ou pelo AppKit

No SwiftUI, anexe .focused($fieldIsFocused) ao editor e defina fieldIsFocused = true em uma ação local, como faz o exemplo. A Apple documenta que atribuir um valor ao estado de foco move o foco para a view associada. Dê bindings de foco distintos a campos separados; atribuir o mesmo valor de foco a vários campos cria ambiguidade. Documentação da Apple sobre FocusState

O foco controla para onde vai a edição. Ele não altera o analisador, não reativa um ancestral desativado nem recria um editor removido pela lógica condicional da view. Resolva essas condições antes de adicionar a recuperação do foco.

Para um campo AppKit que seu app controla, examine NSTextField.isEditable. A Apple define essa propriedade como o controle que determina se o usuário pode editar o valor do campo. Restaure-a para true se seu próprio código a tiver definido incorretamente como falsa, preservando quaisquer restrições intencionais do app. Documentação da Apple sobre NSTextField.isEditable

Ao mover explicitamente o foco no AppKit, verifique acceptsFirstResponder e chame makeFirstResponder(_:) da janela proprietária. O resultado exige cuidado: false pode significar que o respondedor atual se recusou a ceder o foco, enquanto true também pode ocorrer quando o respondedor proposto se recusa e a janela se torna o primeiro respondedor. Verifique o estado resultante da edição. Para selecionar o campo inteiro para substituição, selectText(_:) é a operação documentada; ela não tem efeito antes de o campo pertencer à hierarquia de views de uma janela. Documentação da Apple sobre makeFirstResponder(_:), documentação da Apple sobre selectText(_:)

Respeite o ciclo de vida do editor de campo

O currentEditor() do AppKit retorna o editor de campo atual do controle ou nil quando não há nenhum. A Apple descreve esse editor como um NSTextView compartilhado entre os controles de uma janela e criado quando necessário. Consequentemente, um editor nil fora de uma sessão de edição ativa não demonstra que a entrada numérica esteja com defeito. Documentação da Apple sobre currentEditor()

No nível do sistema de texto, um editor de campo trata Tab, Shift-Tab e Return como sinais para encerrar a edição e possivelmente alterar o primeiro respondedor. Não altere isFieldEditor como correção para conversão numérica: essa propriedade rege o comportamento de edição, não a análise de números opcionais. Restrinja estas verificações do AppKit e do TextKit aos controles que seu app controla. Documentação da Apple sobre NSTextView.isFieldEditor

Verifique se o apagamento e a nova entrada funcionam

Execute esta sequência manual no exemplo completo em cada versão do macOS compatível com seu app. Estes são resultados esperados derivados da lógica do exemplo, não resultados de execuções de testes relatados.

Ação Valor aceito esperado Comportamento esperado da edição
Iniciar 12 O rascunho contém 12
Clicar em Clear nil, exibido como None O rascunho fica vazio; o foco é solicitado
Digitar 24 e clicar em Apply 24 O rascunho contém 24
Substituir por - e pressionar Return Valor aceito anteriormente O rascunho permanece como -; aparece uma mensagem
Substituir por -8 e pressionar Return -8 A mensagem desaparece
Inserir 0 e clicar em Apply 0 Zero permanece distinto da ausência de valor
Inserir 2.5 e clicar em Apply Valor aceito anteriormente O texto fracionário permanece para correção
Esvaziar o campo e clicar em Apply nil O campo continua disponível para outra edição

Teste também colar um inteiro maior que Int.max, clicar fora do campo e voltar, e apagá-lo repetidamente. Em um app integrado, inclua qualquer ação que altere o modelo em outra parte. Essas verificações exercitam separadamente a falha de conversão, a recuperação do foco e a sincronização do rascunho.

A condição de recuperação útil é concreta: depois que o número aceito se torna nil, o editor permanece presente e interativo, e um envio válido posterior fornece um novo número. Manter o rascunho independente torna esse comportamento explícito, sem depender de alterações não documentadas nos controles subjacentes do SwiftUI.

Fontes