В приложении для Mac с редактируемыми строками в одну строку обработайте нажатие Return с помощью onSubmit, вставьте новую модель сразу под текущей строкой и установите фокус на её поле с помощью FocusState. Присвойте каждой строке стабильный идентификатор, чтобы при вставке сохранялась связь между текстом, строкой и фокусом.
В этом руководстве создаётся небольшой локальный редактор списка с примерами записей, например Item A. Нажатие Return создаёт пустую строку под редактируемым полем, сохраняет существующий текст и перемещает клавиатурный фокус на новое поле. Текст в месте ввода не разделяется.
Какой текстовый элемент поддерживает такое поведение?
Используйте однострочный TextField, если каждая строка представляет одну короткую запись. Apple прямо указывает, что TextField вызывает действие onSubmit, когда пользователь нажимает клавишу Return на физической или программной клавиатуре. Apple также указывает, что присваивание значения FocusState перемещает фокус на связанное с ним представление. Оба API доступны в macOS 12 и новее. Apple: onSubmit(of:_:), Apple: FocusState.
Выбирайте элемент управления в зависимости от того, что редактирует пользователь:
| Задача редактирования | С чего начать | Какое поведение Return реализовать |
|---|---|---|
| Одна короткая запись в каждой строке | Однострочный TextField |
Отправить, вставить ниже, установить фокус на новой строке |
| Абзац в каждой строке | TextEditor или текстовое представление AppKit |
Определить, чем вставка строки отличается от вставки новой строки текста |
| Документ с пользовательскими текстовыми командами | NSTextView, встроенный в SwiftUI |
Обработать соответствующую текстовую команду и согласовать изменение модели |
Apple описывает TextEditor как многострочный прокручиваемый редактор. Его документация не устанавливает для него того же поведения «Return отправляет», которое документировано для TextField. Поэтому при замене в примере TextField на TextEditor нужно заново продумать обработку команд; одно лишь добавление того же модификатора не является документированным эквивалентом. Apple: TextEditor, Apple: onSubmit(of:_:).
Разделяйте идентификатор строки и её позицию
Три значения выполняют разные задачи: ID строки идентифицирует запись, индекс в массиве определяет место вставки, а ID в фокусе указывает поле, которое должно принимать ввод с клавиатуры.
ForEach от Apple создаёт представления на основе данных с идентификаторами. Используйте модель Identifiable с ID, сохраняемым при создании строки. В этой схеме тот же ID служит значением фокуса для строки. Благодаря этой связи запись сохраняет свою идентичность, когда над ней вставляется другая запись. Apple: ForEach.
Не используйте редактируемый текст в качестве ID: две пустые строки получат одинаковый идентификатор, а при редактировании он будет меняться. Позиции в массиве тоже плохо подходят на роль идентификаторов при такой вставке, потому что позиции сдвигаются. Это следствия выбранной модели данных, а не дополнительные требования SwiftUI.
Для каждого поля нужно отдельное значение фокуса. Apple предупреждает, что привязка одного и того же значения фокуса к нескольким представлениям создаёт неоднозначную привязку фокуса. Сохранённый UUID для каждой строки позволяет просто различать поля. Apple: FocusState.
Создайте полноценный пример для macOS
Создайте в Xcode проект macOS App на SwiftUI и задайте целевой версией macOS 12 или новее. Замените созданные автоматически точку входа приложения и представление содержимого приведённым ниже файлом, удалив исходное объявление приложения, чтобы в целевом объекте оставалась только одна точка входа @main.
В примере используется обычный VStack внутри прокручиваемого представления. Идентификатор новой строки сохраняется как ожидающая цель фокуса, после чего фокус запрашивается при появлении этого поля. Это стратегия реализации, основанная на API фокуса и появления Apple; Apple не документирует обработчики появления как универсальную гарантию готовности к установке фокуса. Точное время вызова onAppear зависит от представления. Apple: onAppear(perform:).
import SwiftUI
import Foundation
import Dispatch
private struct EditableRow: Identifiable {
let id = UUID()
var text: String
}
@main
struct RowEntryDemoApp: App {
var body: some Scene {
WindowGroup {
RowEditor()
}
}
}
private struct RowEditor: View {
@State private var rows = [
EditableRow(text: "Item A"),
EditableRow(text: "Item B")
]
@State private var pendingFocusID: UUID?
@FocusState private var focusedRowID: UUID?
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Local row editor")
.font(.headline)
Text("Press Return in a field to insert a row below it.")
.font(.subheadline)
ScrollViewReader { proxy in
ScrollView {
VStack(spacing: 10) {
ForEach($rows) { $row in
TextField("New item", text: $row.text)
.textFieldStyle(.roundedBorder)
.focused($focusedRowID, equals: row.id)
.onSubmit {
insertRow(after: row.id)
}
.id(row.id)
.onAppear {
focusIfPending(row.id) {
proxy.scrollTo(row.id, anchor: .center)
}
}
}
}
.padding(4)
}
}
}
.padding(20)
.frame(minWidth: 420, minHeight: 280)
}
private func insertRow(after id: UUID) {
guard pendingFocusID == nil,
let index = rows.firstIndex(where: { $0.id == id })
else { return }
let newRow = EditableRow(text: "")
pendingFocusID = newRow.id
rows.insert(newRow, at: index + 1)
}
private func focusIfPending(
_ id: UUID,
scroll: @escaping () -> Void
) {
guard pendingFocusID == id else { return }
DispatchQueue.main.async {
guard pendingFocusID == id,
rows.contains(where: { $0.id == id })
else { return }
scroll()
focusedRowID = id
pendingFocusID = nil
}
}
}
Модель содержит все примеры данных, а файл определяет каждый используемый тип и функцию. В исследовательской среде не было компилятора Swift и среды выполнения macOS, поэтому программа была проанализирована, но не скомпилирована и не запущена.
Как работают вставка и фокусировка
Строковая привязка поля обновляет соответствующую строку по мере редактирования. Поэтому обработчик отправки может вставить новую строку, не заменяя и не очищая текущую запись. Apple документирует непрерывное обновление текстовых полей, связанных со строкой. Apple: TextField.
Обработчик вставки находит строку, из которой отправлено значение, по ID и вставляет новую по индексу index + 1. Для строки в середине списка новая запись оказывается перед следующей существующей строкой. Для последней строки эта позиция совпадает со значением endIndex массива, что является допустимой позицией для вставки и добавляет элемент в конец. Apple: insert(_:at:).
Обработчик появления нового поля проверяет, совпадает ли его ID с ожидающим запросом. Отложенный блок ещё раз проверяет это перед прокруткой и установкой фокуса. Эти проверки не позволяют постороннему обработчику появления использовать запрос. В примере также игнорируются повторные отправки, пока ожидается запрос фокуса.
Дополнительный вызов dispatch откладывает работу до выхода из обработчика появления; он не является документированным барьером отрисовки. Проверьте этот выбор планирования на поддерживаемых версиях macOS и с фактической иерархией контейнеров.
Прокрутка и фокусировка — разные операции. scrollTo находит дочерний элемент по идентификатору представления и прокручивает к нему; присваивание focusedRowID запрашивает клавиатурный фокус. Явный .id(row.id) задаёт цель прокрутки. Apple: scrollTo(_:anchor:).
Проверьте результат и разберитесь с распространёнными проблемами
Проверьте пример с помощью следующей последовательности:
- Щёлкните
Item A, добавьте текст и один раз нажмитеReturn. - Убедитесь, что между двумя исходными записями появилось одно пустое поле.
- Сразу введите
Item Cи убедитесь, что текст появился в новом поле. - Нажмите
Returnв последнем поле и убедитесь, что новая строка появилась в конце. - Нажмите
Returnв пустом поле и убедитесь, что можно добавить ещё одну пустую строку. - Продолжайте, пока не понадобится прокрутка, затем убедитесь, что вставленное поле становится видимым и принимает ввод.
Это ожидаемые результаты, которые нужно проверить локально. Пример намеренно допускает пустые записи и сохраняет всю текущую строку, даже если курсор стоит посередине. Если нужно разделить эту строку на две записи, дополнительно потребуется обрабатывать выделение и диапазоны текста.
Если строка появляется, но ввод остаётся в старом поле, проверьте ID в фокусе, обработчик появления нового поля и активность окна. Также проверьте, не сбрасывает ли фокус другой обработчик. Не создавайте новые ID строк при вычислении представления.
Если вставка происходит дважды, проверьте обработчики отправки в вышестоящих представлениях. Apple разрешает использовать onSubmit как для отдельных элементов управления, так и для содержащих их представлений, а submitScope документирован как способ остановить передачу отправки вышестоящим обработчикам. Назначьте действие вставки одному обработчику. Apple: onSubmit(of:_:).
Изменение метки отправки не реализует переход или вставку. Apple определяет submitLabel как установку семантической метки отправки. Изменение модели и установку фокуса всё равно нужно реализовать отдельно. Apple: submitLabel(_:).
Для гораздо более крупной коллекции заново оцените целесообразность обычного стека. Apple отмечает, что такие контейнеры, как List и LazyVStack, лениво запрашивают элементы ForEach. Поэтому для выполнения запроса фокуса может понадобиться сначала вывести находящееся за экраном поле в видимую область, чтобы сработал его обработчик появления. Не заменяйте контейнер, не проверив эту последовательность. Apple: ForEach.
Когда стоит использовать AppKit и TextKit?
Используйте пример на SwiftUI для коротких независимых записей. Если строки содержат текст в несколько строк или требуют точной обработки текстовых команд, можно встроить собственное NSTextView через NSViewRepresentable. Apple описывает representable как мост для создания и обновления представлений AppKit внутри SwiftUI, а координатор — как средство передачи взаимодействий с делегатом. Apple: NSViewRepresentable.
В этом варианте сохраните ту же модель строк с ID и реализуйте такую последовательность:
- Создайте текстовое представление и назначьте координатор его делегатом.
- Передавайте изменения текста в модель соответствующей строки.
- Реализуйте
textView(_:doCommandBy:)и распознавайте командуinsertNewline(_:)согласно выбранному поведению редактора. - Если при обработке этой команды вставляется строка, сохраните текущий текст, вставьте новую строку и верните
true, чтобы обработать команду. Для команд, которые делегат не обрабатывает, вернитеfalse. - После подключения нового текстового представления к содержащему его окну запросите фокус с помощью метода
makeFirstResponder(_:)этого окна.
Apple документирует и команду новой строки, и логическое соглашение для делегата: true означает, что делегат обработал команду, а false позволяет текстовому представлению попытаться выполнить её. Это обеспечивает явный путь обработки команд для редактора, принадлежащего приложению. Apple: insertNewline(_:), Apple: textView(_:doCommandBy:).
При добавлении пользовательской обработки команд сохраняйте активную композицию текста. AppKit предоставляет hasMarkedText() для проверки наличия помеченного текста; учитывайте это состояние, решая, должен ли делегат передать команду текстовой системе. Проверьте композицию в готовом редакторе, а не исходите из предположения, что каждое взаимодействие с Return должно вставлять строку. Apple: hasMarkedText().
Для установки фокуса Apple прямо предписывает использовать makeFirstResponder(_:) содержащего окна, а не вызывать напрямую becomeFirstResponder() текстового представления. У доступа к разметке TextKit есть особенность, зависящая от версии: в macOS 12 и новее прямой доступ к layoutManager текстового представления переключает его в режим совместимости с использованием NSLayoutManager. Для самой вставки строк такой доступ не требуется. Apple: NSTextView.
Для задачи с однострочными записями начните с полного примера на SwiftUI и проверьте в приложении для Mac вставку, видимость и ввод текста. Стабильная идентичность, явная позиция вставки и адресный запрос фокуса обеспечивают основное поведение; используйте текстовое представление AppKit, когда задаче редактирования нужны его механизмы управления командами и выделением.
Источники
- onSubmit(of:_:) — Apple
- FocusState — Apple
- TextField — Apple
- TextEditor — Apple
- ForEach — Apple
- onAppear(perform:) — Apple
- insert(_:at:) — Apple
- scrollTo(_:anchor:) — Apple
- submitLabel(_:) — Apple
- NSViewRepresentable — Apple
- insertNewline(_:) — Apple
- textView(_:doCommandBy:) — Apple
- hasMarkedText() — Apple
- NSTextView — Apple