Чтобы числовое поле оставалось доступным для редактирования после того, как его необязательное значение стало nil, храните редактируемый текст отдельно от числа. Сначала проверьте разбор значения, отключённое состояние и фокус, а затем используйте черновик на основе строки, если приложению нужно явно обрабатывать восстановление.

Это руководство посвящено числовому полю в разрабатываемом вами приложении для macOS: вводу условного значения Tile count, его очистке и вводу другого числа. Цель — сохранить осмысленное пустое значение и при этом разрешить следующий ввод.

Почему необязательное число стало nil?

Apple предоставляет специальный инициализатор SwiftUI для необязательных значений: TextField(value:format:prompt:label:). Согласно документации, если формат не может разобрать введённые данные, привязке присваивается nil. Поэтому неудачное преобразование — ожидаемое состояние при использовании этого API. Apple не описывает это состояние как отключение поля. Для этого инициализатора с необязательным значением и стилем форматирования требуется macOS 12 или новее. Инициализатор TextField для необязательных значений от Apple

Проверьте, какой именно инициализатор использует ваше приложение. У более старого инициализатора formatter: поведение при ошибке, описанное в документации, отличается: если преобразование не удаётся, связанное значение остаётся без изменений. Замена formatter: на format: меняет это поведение, поэтому рассматривайте её как осознанное изменение, а не универсальное исправление. Инициализатор TextField с форматтером от Apple

Проблема в самой модели данных: необязательное число не может представить все состояния редактирования. Пустое поле, частично введённый знак минуса и недопустимая строка — всё это может не соответствовать пригодному целому числу, но такие черновики текста различаются. Поэтому полезно хранить отдельный черновик: поле может сохранять именно тот текст, который редактирует пользователь, а приложение — решать, когда принимать числовое значение.

Прежде чем менять поле, выясните причину сбоя

Выполните следующие действия в окне своего приложения. Они помогут отличить проблему преобразования от проблемы взаимодействия, не предполагая, что дело в конкретном дефекте реализации macOS.

  1. Проверьте, меняется ли текст. Щёлкните поле, выделите его содержимое и введите 24. Если символы меняются, но числовая модель — нет, проверьте преобразование и обработку подтверждения ввода.
  2. Проверьте условия взаимодействия. Найдите в коде поля и его контейнеров условия вроде .disabled(tileCount == nil). Такое условие не позволяет выполнить редактирование, необходимое для восстановления после очистки значения.
  3. Проверьте, существует ли поле. Если редактор создаётся только внутри ветви if let, присваивание значению nil может удалить редактор. Храните черновик и редактор независимо от необязательного числа.
  4. Отдельно проверьте фокус. Если поле реагирует на щелчок и после этого ввод работает, восстановите фокус с помощью соответствующего публичного API.
  5. Упростите пример. Временно удалите пользовательские преобразования привязки и синхронизацию модели. Воспроизведите переход от 12 к пустому значению, а затем к 24 в небольшом представлении.

Имеет значение и отключённое состояние родительского элемента: Apple указывает, что .disabled(true) контейнера переопределяет .disabled(false) дочернего элемента. Исправьте условие контейнера, а не включайте поле снова и снова. Документация Apple по disabled(_:)

Также проверьте сеттеры пользовательских привязок. Сеттер, который сразу возвращает управление, если текущее число равно nil, не сможет принять новое число. Аналогично, преобразование каждой ошибки разбора в 0 стирает различие между отсутствующим значением и настоящим нулём. Это решения в логике приложения; смена фокуса их не исправит.

Храните редактируемый черновик и явно подтверждайте число

Поле, привязанное к строке, позволяет отдельно управлять тремя состояниями: текущим текстом, принятым необязательным числом и сообщением проверки. Согласно документации Apple, TextField, привязанный к строке, непрерывно обновляет привязку во время редактирования. Поэтому он подходит для хранения незавершённого ввода. Обзор TextField от Apple

В приведённом ниже полном примере используется следующая логика: пустой ввод означает nil; допустимое целое число в десятичной системе заменяет принятое значение; недопустимый ввод остаётся видимым, а принятое значение не меняется. Кнопка Clear очищает оба состояния и запрашивает фокус, чтобы можно было ввести другое число.

Создайте в Xcode проект приложения macOS на SwiftUI с целевой версией macOS 12 или новее. Замените сгенерированные объявления приложения и представления содержимого одним этим файлом, оставив в цели только одно объявление @main. В примере используются только собственное окно и условные подписи. В исследовательской среде не было Swift и SDK для macOS, поэтому код проверили, но не компилировали и не запускали.

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

Поле всегда привязано к draft, поэтому присваивание tileCount значения nil не удаляет состояние редактируемого текста. Преобразование выполняется только после нажатия кнопки Apply или клавиши Return. После отправки недопустимого значения сообщение остаётся до следующей отправки или нажатия Clear; черновик доступен для исправления.

guard не даёт неудачному преобразованию заменить принятое число. Успешная отправка нормализует запись с помощью String(parsed): например, при вводе +024 получается 24. Это выбранная для примера логика. Удалите присваивание с нормализацией, если важно сохранить исходное написание.

Осознанно выберите правила записи числа

Этот пример принимает целые числа со знаком без разделителей, в десятичной системе счисления. Он отклоняет пробелы, десятичные дроби и разделители разрядов. В документации Swift указано, что Int(_:radix:) возвращает nil для недопустимого ввода или значения, которое не помещается в тип целого числа; поэтому слишком большое вставленное число тоже будет отклонено при проверке. Инициализатор строки для Int в Swift

Если поле должно поддерживать локализованную запись чисел, сохраните ту же структуру с черновиком и подтверждением, но замените преобразование подходящей стратегией разбора Foundation. У ParseableFormatStyle есть свойство parseStrategy, а метод стратегии parse(_:) выполняет преобразование и может выбросить ошибку. Обработайте ошибку, сохранив черновик. Выберите согласованные правила форматирования и разбора, а затем проверьте нужные локали. Документация Apple по ParseableFormatStyle, документация Apple по ParseStrategy

Если другая часть приложения может заменить принятое число, определите, как такое обновление повлияет на незавершённый черновик. Например, действие Load value может намеренно обновить оба состояния. Не обновляйте черновик непрерывно из принятого числа во время редактирования: так вы потеряете незавершённый текст, который позволяет сохранить эта схема.

Восстановите фокус средствами SwiftUI или AppKit

В SwiftUI добавьте к редактору .focused($fieldIsFocused) и задайте fieldIsFocused = true в локальном действии, как это сделано в примере. Apple указывает, что присвоение значения состоянию фокуса переводит фокус на связанное с ним представление. Для разных полей используйте отдельные привязки фокуса: присвоение одного значения фокуса нескольким полям создаёт неоднозначность. Документация Apple по FocusState

Фокус определяет, куда направляется ввод. Он не меняет правила разбора, не включает отключённый родительский элемент и не создаёт заново редактор, удалённый условной логикой представления. Прежде чем добавлять восстановление фокуса, устраните эти причины.

Если приложение владеет полем AppKit, проверьте NSTextField.isEditable. Apple определяет это свойство как управляющее возможностью пользователя редактировать значение поля. Если ваш код по ошибке присвоил ему значение false, восстановите true, сохранив при этом все намеренные ограничения приложения. Документация Apple по NSTextField.isEditable

При явном переводе фокуса средствами AppKit проверьте acceptsFirstResponder и вызовите makeFirstResponder(_:) окна-владельца. Результат требует внимания: false может означать, что текущий ответчик отказался передать фокус, а true также может появиться, когда предложенный ответчик отказывается принять фокус и окно становится первым ответчиком. Проверьте, перешло ли поле в нужное состояние редактирования. Чтобы выделить всё поле для замены, используйте документированный метод selectText(_:); он не действует, пока поле не включено в иерархию представлений окна. Документация Apple по makeFirstResponder(_:), документация Apple по selectText(_:)

Учитывайте жизненный цикл редактора поля

Метод AppKit currentEditor() возвращает текущий редактор поля или nil, если редактора нет. Apple описывает этот редактор как NSTextView, общий для элементов управления в окне и создаваемый по мере необходимости. Поэтому наличие редактора nil вне активного сеанса редактирования не означает, что ввод чисел сломан. Документация Apple по currentEditor()

На уровне текстовой системы редактор поля рассматривает Tab, Shift-Tab и Return как сигналы завершить редактирование и, возможно, сменить первый ответчик. Не меняйте isFieldEditor в качестве способа исправить преобразование числа: это свойство управляет поведением редактирования, а не разбором необязательного числа. Проверки AppKit и TextKit ограничивайте элементами управления, которыми владеет ваше приложение. Документация Apple по NSTextView.isFieldEditor

Убедитесь, что после очистки можно снова вводить данные

Выполните эту последовательность вручную на каждой версии macOS, которую поддерживает приложение, используя полный пример. Это ожидаемые результаты, следующие из логики примера, а не отчёт о проведённых тестах.

Действие Ожидаемое принятое значение Ожидаемое поведение при редактировании
Запуск 12 Черновик содержит 12
Нажать Clear nil, отображается как None Черновик пуст; запрашивается фокус
Ввести 24, затем нажать Apply 24 Черновик содержит 24
Заменить на -, затем нажать Return Прежнее принятое значение Черновик остаётся равным -; появляется сообщение
Заменить на -8, затем нажать Return -8 Сообщение исчезает
Ввести 0, затем нажать Apply 0 Ноль остаётся отличным от отсутствующего значения
Ввести 2.5, затем нажать Apply Прежнее принятое значение Дробный текст остаётся для исправления
Очистить поле, затем нажать Apply nil Поле остаётся доступным для следующего ввода

Также проверьте вставку целого числа, превышающего Int.max, щелчок за пределами поля с последующим возвратом к нему и многократную очистку поля. В интегрированном приложении добавьте действие, меняющее модель в другом месте. Эти проверки позволяют по отдельности проверить ошибки преобразования, восстановление фокуса и синхронизацию черновика.

Восстановление можно считать успешным при выполнении конкретного условия: после того как принятое число стало nil, редактор остаётся доступным и интерактивным, а следующая отправка допустимого значения задаёт новое число. Независимое хранение черновика явно обеспечивает такое поведение и не требует полагаться на недокументированные изменения внутренних элементов управления SwiftUI.

Источники