Чтобы вставить вариант, не применяя значение поля, разделите автодополнение и отправку. В macOS 15 и более поздних версиях SwiftUI предоставляет встроенные текстовые подсказки; используйте submitScope(), чтобы блокировать обработчики отправки в родительских представлениях, а для завершающего действия добавьте отдельную кнопку.
В этом руководстве создаётся небольшой выборщик меток для собственного приложения Mac. При вводе Bl предлагаются искусственные метки, например Blue Tile. Выбор варианта обновляет редактируемый черновик; нажатие Apply Label копирует черновик в применённое значение. Это различие важно: предотвращение действия приложения и перехват команды клавиши Return — разные требования.
Выберите API в соответствии с взаимодействием
Модификатор textInputSuggestions(_:) от Apple показывает варианты для текстового поля. Для каждого выбираемого варианта требуется textInputCompletion(_:), задающий строку для подстановки. Оба API документированы для macOS 15 и более поздних версий. Вариант без модификатора автодополнения может отображаться, но не реагирует на нажатия. Apple: textInputSuggestions(_:), Apple: textInputCompletion(_:)
Для короткого поля метки уместно заменять весь черновик. В документации Apple по подсказкам описана такая замена всего поля. Не считайте, что этот API предоставляет редактор токенов с учётом позиции курсора или предварительный просмотр встроенного суффикса: для этого нужна более специализированная схема редактирования. Apple: textInputSuggestions(_:)
| Требование | Подходящая отправная точка |
|---|---|
| Предлагать полные метки в macOS 15 и более поздних версиях | Встроенные текстовые подсказки SwiftUI |
| Не допускать вызова обработчика отправки в родительском представлении из поля | submitScope() для поля |
| Использовать собственную компоновку подсказок или поддерживать более раннюю версию системы | Кнопки подсказок приложения, обновляющие привязку черновика |
| Принимать выделенный элемент по клавише Return и перехватывать эту команду | Собственный элемент AppKit с обработкой команд |
| Предварительно показывать суффикс или заменять слово в позиции ввода | Автодополнение текста AppKit и явный диапазон автодополнения |
Последние два пункта — архитектурные решения, основанные на документированных API команд и автодополнения. Они полезны, когда взаимодействию нужен более точный контроль, чем обеспечивают строка для подстановки и граница отправки.
Создайте полноценный выборщик меток на SwiftUI
Создайте в Xcode проект приложения macOS на SwiftUI с SDK, включающим API macOS 15. Установите целевую версию развёртывания macOS 15 или выше. Замените сгенерированные объявления приложения и представления содержимого приведённым ниже единым исходным файлом, оставив в целевом модуле только одно объявление приложения @main.
В примере draft и appliedLabel хранятся отдельно. Локальное правило сопоставления намеренно простое: сравнение префиксов с учётом регистра, отсутствие подсказок для пустого текста и отсутствие подсказки при точном совпадении. В документации Swift hasPrefix(_:) описан как сравнение строкового префикса; более сложные правила сопоставления остаются на усмотрение приложения. 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
}
}
}
Это полноценный пример приложения со встроенными демонстрационными метками и состоянием. Его сверили с полученной документацией API, но здесь не компилировали и не запускали, поскольку инструментарий Swift и среда выполнения пользовательского интерфейса macOS были недоступны.
Почему выбор подсказки не применяет метку
Каждый вариант автодополнения передаёт строку текстовому полю, привязанному к draft. Единственный код, присваивающий значение appliedLabel или увеличивающий applyCount, находится в обработчике нажатия кнопки Apply Label. Поэтому в этом примере обновление черновика никак не приводит к завершающему действию.
Кроме того, у поля задан submitScope(). По документации Apple, этот модификатор не позволяет триггерам отправки передаваться модификаторам отправки выше по иерархии представлений. Это важно, если позднее компонент будет встроен в более крупную форму с родительским onSubmit. Apple: submitScope(_:)
Начальный фокус задаётся через @FocusState. Apple указывает, что программное присваивание значения этой привязке перемещает фокус на связанное с ней представление. Здесь фокус служит для удобства редактирования и не влияет на применение метки. Apple: FocusState
Что пример намеренно допускает
Список меток предлагает варианты, но не ограничивает допустимый ввод. При вводе Square Tile кнопка Apply Label всё равно становится активной. Если интерфейс должен разрешать только метки из списка, явно измените правило проверки — например, активируйте кнопку, только когда черновик совпадает с записью каталога. Не заменяйте молча черновик без совпадений первым вариантом.
Пример также заменяет метку целиком. Если в черновике несколько слов, выбор варианта следует понимать как выбор итогового значения поля. Для редактора токенов, который должен сохранять окружающий текст, требуется замена с учётом выделения.
Разберитесь, что значит «без отправки»
В документации SwiftUI для onSubmit(of:_:) сказано, что текстовое поле вызывает действие отправки, когда пользователь нажимает Return. Действия отправки можно задавать как для отдельного представления, так и для всей иерархии. Если выбор варианта автодополнения неожиданно запускает действие приложения, проверьте оба места. Apple: onSubmit(of:_:)
submitScope() задаёт границу для обработчиков в родительских представлениях. В документации не обещано, что внутреннее событие отправки не возникает, что редактирование остаётся активным или что перехватывается каждая клавиатурная команда. Аналогично, в документации Apple для модификаторов автодополнения описана замена текста, но не универсальное подавление поведения клавиши Return. Учитывайте эти ограничения при выборе взаимодействия. Apple: submitScope(_:), Apple: textInputCompletion(_:)
Для существующего компонента проверьте эти пути по отдельности:
- Удалите завершающее действие приложения из собственного
onSubmitполя, если Return никогда не должна применять его значение. - Добавьте к полю
submitScope(), чтобы блокировать действие отправки в родительском представлении. - Отдельно проверьте сочетания клавиш кнопок, особенно явное действие по умолчанию.
- Проверьте наблюдатели изменений и установщики модели, которые выполняют завершающее действие при каждом изменении черновика.
Пустой замыкатель onSubmit в локальном представлении не является документированной заменой границы отправки. Apple специально предоставляет submitScope() для остановки распространения события. Если приложение должно отправлять данные по Return, только когда автодополнение не активно, такую условную политику клавиатуры нужно дополнить состоянием и обработкой команд. Apple: onSubmit(of:_:)
Используйте AppKit, если Return должна принимать вариант и перехватываться
Для собственного NSTextField в AppKit предусмотрен control(_:textView:doCommandBy:). Аргумент text-view — это редактор поля данного элемента управления. Делегат может обработать связанную команду и вернуть true; возврат false означает, что делегат её не обработал. Это документированная точка расширения для настройки команд текстового поля. Apple: control(_:textView:doCommandBy:)
Собственный контроллер автодополнения может использовать эту точку расширения в следующем порядке:
- Отслеживать, открыто ли окно подсказок и какой элемент выделен.
- Когда поступает команда Return и выделен допустимый элемент, вставить этот элемент в черновик.
- Закрыть окно подсказок и синхронизировать модель черновика приложения.
- Вернуть
true, поскольку контроллер обработал команду. - Для команд, которые контроллер не обрабатывает, вернуть
false.
Это последовательность действий, основанная на контракте делегата, а не встроенная политика автодополнения. Отдельно решите, что должна делать Return, когда список закрыт. Если вместо этого вы используете отдельный NSTextView, соответствующая точка перехвата — textView(_:doCommandBy:); по утверждению Apple, возврат true не позволяет текстовому представлению самостоятельно выполнить эту команду. Apple: textView(_:doCommandBy:)
Для самого автодополнения текста NSTextView.insertCompletion(_:forPartialWordRange:movement:isFinal:) поддерживает замену и изменение выделения. При isFinal: false выделяется предложенный суффикс; при isFinal: true выделение сворачивается после завершённого слова. Этот флаг описывает состояние автодополнения, а не отправку данных приложением. Аргумент movement указывает, каким способом выполнялась навигация по вариантам или их выбор. Apple: insertCompletion(_:forPartialWordRange:movement:isFinal:)
Сохраняйте правильный текстовый диапазон
В AppKit rangeForUserCompletion задаёт частичное слово от его начала до позиции ввода. Используйте диапазон, соответствующий предполагаемой замене, и пересчитывайте его после редактирования или перемещения курсора. Не используйте вслепую ранее сохранённый диапазон после изменения текста. Apple: rangeForUserCompletion
При преобразовании текстовых диапазонов не подставляйте количество символов Swift вместо длины NSString. В документации Swift объясняется, что String считает расширенные графемные кластеры, а NSString считает кодовые единицы UTF-16. Эти значения могут различаться. Замена всего содержимого привязки в примере SwiftUI полностью устраняет необходимость такого преобразования диапазонов. Swift: Strings and Characters)
Проверяйте вставку и применение отдельно
Для каждой поддерживаемой версии macOS проверяйте результат по видимому черновику, применённому значению и счётчику. Далее перечислены ожидаемые проверки для примера, а не заявленные результаты тестирования.
| Действие | Ожидаемый наблюдаемый результат |
|---|---|
Ввести Bl |
Черновик — Bl; отображаются подходящие метки |
Нажать Blue Tile |
Черновик становится Blue Tile; применённое значение и счётчик не меняются |
| Нажать Return во время редактирования | Приложение не применяет черновик |
Нажать Apply Label |
Применённое значение копируется из черновика; счётчик увеличивается на единицу |
| Изменить черновик после этого | Предыдущее применённое значение остаётся видимым |
| Очистить черновик | Список подсказок пуст, кнопка Apply Label неактивна |
Ввести bl |
Совпадений нет из-за заданного в примере сравнения с учётом регистра |
Также проверьте выбор вариантов с клавиатуры во встроенном списке подсказок и повторите проверки после встраивания поля в родительское представление приложения. Apple отмечает, что такие события, как перемещение окна macOS, могут закрыть встроенный интерфейс подсказок, поэтому черновик должен оставаться доступным и без него. Apple: textInputSuggestions(_:)
Если целевая версия развёртывания ниже macOS 15, используйте встроенное представление с проверкой доступности и резервный вариант либо повсеместно применяйте кнопки подсказок приложения. Резервная кнопка может напрямую присвоить её метку черновику, оставив завершающее действие за кнопкой Apply Label. Это обеспечит то же разделение черновика и применения; навигацию с клавиатуры и отображение придётся реализовать в компоненте.
Для этой задачи с выбором метки важна чёткая граница, которую можно наблюдать: принятие варианта автодополнения меняет редактируемый черновик, а применённую метку меняет только явное завершающее действие. Для этого используйте встроенные подсказки и область отправки SwiftUI; переходите к обработке команд AppKit, если требование включает перехват самой клавиши Return.
Источники
- 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