Для пользовательской коллекции с одним столбцом в приложении для Mac используйте SwiftUI List с привязкой выбора, основанной на наборе стабильных идентификаторов элементов. Настройте строки, оставив нативному элементу управления обработку выбора диапазона и выбора с помощью Command-щелчка.
В этом руководстве создается коллекция условных меток от Tile 01 до Tile 12. Пользователь должен выбрать соседние элементы, добавлять или удалять отдельные элементы и сохранять понятное представление о выборе при фильтрации или изменении порядка отображения на обратный. В одном из следующих разделов объясняется подход с AppKit для коллекции, которой нужна сетка или другая пользовательская компоновка.
Как должен работать выбор диапазона и элементов с помощью Command-щелчка?
В руководстве пользователя Mac от Apple описано привычное взаимодействие: щелкните первый элемент, затем щелкните последний, удерживая Shift, чтобы выбрать соседние элементы, включая все промежуточные. Command-щелчок выбирает отдельные элементы, которые не обязаны быть соседними; Command-щелчок по уже выбранному элементу удаляет его из множественного выбора. Для этой коллекции нужно сохранить именно эти базовые взаимодействия. Выбор элементов на экране Mac.
В начальной реализации используется один упорядоченный список. Благодаря этому понятие «между» однозначно: это строки между двумя крайними элементами в текущем порядке отображения. Пространственная коллекция требует дополнительного решения: должен ли диапазон следовать последовательности данных или области компоновки. Это заслуживает отдельного тестирования.
Выбирайте элемент управления с учетом того, что выбирает пользователь:
| Задача пользователя | Подходящий вариант для начала | Представление выбора |
|---|---|---|
| Выбирать целые пользовательские строки | SwiftUI List |
Набор идентификаторов элементов |
| Выбирать целые плитки в пользовательской компоновке | AppKit NSCollectionView |
Индексные пути выбранных элементов, сопоставленные с идентификаторами модели |
| Выбирать символы в метке | Выбор текста SwiftUI | Выбор текста, предоставляемый текстовым представлением |
| Выбирать диапазоны в текстовом документе | Текстовая система AppKit | Диапазоны символов или текста |
Apple описывает List как контейнер с одним столбцом и необязательным выбором, а NSCollectionView — как упорядоченную коллекцию с настраиваемой компоновкой. Это рекомендуемые отправные точки для выбора элементов. List, NSCollectionView.
Создайте полный пример на SwiftUI
Создайте в Xcode проект приложения для macOS на SwiftUI. Для этого примера задайте целевую версию macOS 14 или новее: используемое ниже замыкание onChange с двумя параметрами требует macOS 14. Замените сгенерированное объявление приложения этим кодом, оставив в целевом объекте ровно одну точку входа @main. onChange(of:initial:_:), App.
Ниже приведено небольшое законченное приложение, включая условные данные и окно. Оно не использует внешние ресурсы. Пример сверили с документированными API, но здесь его не компилировали и не запускали в macOS.
import Foundation
import SwiftUI
struct Tile: Identifiable {
let id: Int
let title: String
}
@main
struct CollectionSelectionDemoApp: App {
var body: some Scene {
WindowGroup {
TileCollectionView()
}
}
}
struct TileCollectionView: View {
private let tiles: [Tile] = (1...12).map {
Tile(id: $0, title: String(format: "Tile %02d", $0))
}
@State private var selectedIDs: Set<Int> = []
@State private var query = ""
@State private var reversed = false
private var visibleTiles: [Tile] {
let filtered = tiles.filter {
query.isEmpty ||
$0.title.localizedCaseInsensitiveContains(query)
}
return reversed ? Array(filtered.reversed()) : filtered
}
private var selectionDescription: String {
let names = visibleTiles
.filter { selectedIDs.contains($0.id) }
.map(\.title)
return names.isEmpty ? "None selected" : names.joined(separator: ", ")
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
TextField("Filter tiles", text: $query)
.textFieldStyle(.roundedBorder)
HStack {
Button("Reverse order") {
reversed.toggle()
}
Button("Clear selection") {
selectedIDs.removeAll()
}
.disabled(selectedIDs.isEmpty)
}
List(visibleTiles, selection: $selectedIDs) { tile in
HStack(spacing: 12) {
RoundedRectangle(cornerRadius: 4)
.fill(Color.gray.opacity(0.2))
.frame(width: 32, height: 32)
VStack(alignment: .leading) {
Text(tile.title)
Text("Sample item \(tile.id)")
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
}
.padding(.vertical, 4)
.tag(tile.id)
}
Text("\(selectedIDs.count) selected")
Text(selectionDescription)
.font(.caption)
}
.padding()
.frame(minWidth: 440, minHeight: 480)
.onChange(of: query) { _, _ in
let visibleIDs = Set(visibleTiles.map(\.id))
selectedIDs.formIntersection(visibleIDs)
}
}
}
Тип элемента в привязке выбора совпадает с Tile.ID: в обоих случаях используется Int. Согласно документации Apple, привязка выбора списка к набору типа идентификатора его данных включает множественный выбор. На Mac для множественного выбора не требуется переходить в режим редактирования. Поддержка выбора в списках.
Строку по-прежнему можно настраивать: в ней есть нейтральная фигура, заголовок и дополнительная метка. Обработчик щелчка на уровне строки и отдельная операция переключения не используются. Если выбором управляет сам список, две независимые части кода не пытаются обновить его в ответ на одно взаимодействие.
Отделяйте идентичность элементов от порядка отображения
В примере намеренно разделены три понятия: упорядоченный массив элементов, набор выбранных идентификаторов и отфильтрованное представление массива. В Swift массивы определены как упорядоченные коллекции, а множества — как коллекции уникальных значений без заданного порядка. Поэтому множество подходит для хранения факта включения элемента в выбор, а массив задает порядок отображения. Типы коллекций.
Целочисленные идентификаторы неизменны для этого набора условных данных. Обращение массива меняет расположение элемента, но не его идентификатор. В реальной модели присваивайте каждому элементу стабильный идентификатор при создании или загрузке и сохраняйте его при последующих обновлениях представления. Не генерируйте новый идентификатор при каждой проверке строки.
Сводка выбора следует порядку visibleTiles и проверяет наличие элемента в selectedIDs. Это решение основано на перечисленных свойствах коллекций: сводка формируется в порядке отображения, а не в порядке перебора множества.
Для фильтрации требуется явное продуктовое решение. В этом примере применяется политика «только видимые элементы»: при каждом изменении фильтра выбор пересекается с идентификаторами элементов, которые остаются видимыми. Скрытые элементы теряют статус выбранных, и при снятии фильтра прежний выбор не восстанавливается. Метки и исходный массив остаются без изменений.
Например, выберите Tile 02, Tile 03 и Tile 04. Введите 04 в поле Filter tiles. Согласно политике этого примера, выбранным останется только Tile 04. Удалите фильтр — два других элемента снова появятся, но уже невыбранными. Это иллюстративное описание работы кода, а не сообщение о фактическом выполнении.
Другому приложению может потребоваться сохранять выбор при фильтрации. В таком случае перенесите управление выбором в модель, определите, как учитывать скрытые выбранные элементы в количестве и сводке, и проверьте согласование с отображаемым элементом управления. Одного удаления операции пересечения недостаточно, чтобы определить полноценную политику для скрытого выбора.
Когда следует использовать представление коллекции AppKit?
Используйте NSCollectionView, если коллекции нужны плитки в нескольких столбцах или компоновка, отличная от обычного списка. Apple разделяет источник данных коллекции, контроллеры представлений элементов, делегат и объект компоновки. Так можно настраивать внешний вид и расположение, оставляя обработку взаимодействий за элементом управления коллекцией. NSCollectionView.
Настраивайте реализацию AppKit в таком порядке:
- Создайте представление коллекции и назначьте ему современную компоновку, например
NSCollectionViewFlowLayout. - Укажите источник данных и зарегистрируйте класс элемента или перо, используемые для создания ячеек.
- Задайте
isSelectableзначениеtrue, аallowsMultipleSelection— значениеtrue. - Установите
allowsEmptySelectionв соответствии с тем, допускает ли задача отсутствие выбора. - Прочитайте
selectionIndexPaths, а затем сопоставьте каждый допустимый индексный путь с соответствующим идентификатором модели. - Отобразите выбор в представлении элемента с помощью его состояния
isSelected.
По умолчанию множественный выбор отключен. Поэтому его включение — явный этап настройки, а не следствие того, что плитки выглядят выбираемыми. Свойство элемента isSelected сообщает, выбран ли этот элемент. allowsMultipleSelection, isSelected.
Считайте индексные пути расположениями в текущем источнике данных. Apple указывает, что selectionIndexPaths обновляется при интерактивном выборе, а также может задаваться программно. Если назначить индексный путь, который не соответствует допустимому элементу источника данных, возникнет исключение. После изменения порядка или фильтрации заново формируйте расположения выбранных элементов по стабильным идентификаторам вместо повторного использования устаревших позиций. selectionIndexPaths.
Соблюдайте границы документированного поведения: allowsMultipleSelection позволяет выбирать несколько элементов; в документации этого свойства не описан полный алгоритм выбора диапазона для любой пользовательской компоновки или комбинации модификаторов. Проверьте поведение Shift-щелчка в фактической компоновке и поддерживаемых версиях macOS. Если задача требует точного линейного диапазона, более понятной отправной точкой будет приведенная выше реализация с одним столбцом.
Чтобы использовать элемент управления AppKit в приложении SwiftUI, встроите его через NSViewRepresentable. Создайте иерархию AppKit в makeNSView, согласуйте изменения модели в updateNSView и используйте координатор для передачи событий делегата в состояние SwiftUI. SwiftUI управляет рамкой и границами представленного представления. NSViewRepresentable.
Для программных обновлений также требуется явная синхронизация. Apple указывает, что collectionView(_:shouldSelectItemsAt:) и collectionView(_:didSelectItemsAt:) не вызываются при программном задании выбора. Объедините обновления модели в общей операции и не рассчитывайте, что эти обратные вызовы будут запускаться после каждого присваивания. collectionView(_:shouldSelectItemsAt:), collectionView(_:didSelectItemsAt:).
Дает ли выбор текста такую же возможность?
textSelection(.enabled) в SwiftUI управляет выбором текста внутри представлений. Если применить его к контейнеру, доступным для выбора станет текст во всех вложенных представлениях. Документация не описывает ни привязку выбора элементов коллекции, ни общую модель выбора диапазона для произвольных карточек. Используйте этот механизм, если нужно выбирать символы в метке. textSelection(_:).
Для интерфейса текстового документа AppKit предоставляет выбор символов через NSTextView.selectedRanges. Его диапазоны упорядочены, не пересекаются и могут быть несмежными. NSTextSelection.textRanges в TextKit также представляет логически упорядоченные текстовые диапазоны и доступен начиная с macOS 12. Эти API описывают позиции в тексте, поэтому их сопоставление с идентификаторами плиток зависит от конкретного приложения. selectedRanges, textRanges.
Прежде чем включать выбор текста внутри метки строки, решите, какую роль должен выполнять жест на этой метке. В рамках задачи этого руководства щелчок по метке должен участвовать в выборе всей строки, поэтому в примере выбор текста не настраивается.
Проверьте взаимодействия по небольшой матрице выбора
Выполните эти проверки в окне самого приложения. Первые четыре проверяют основные принципы выбора на Mac; остальные — решения примера, связанные с идентификаторами и фильтрацией. Документированное Apple взаимодействие служит основой, а условные метки позволяют легко проверить ожидаемый состав выбора. Выбор элементов на экране Mac.
| Действие | Ожидаемый результат |
|---|---|
Щелкнуть Tile 02 |
Выбран один элемент |
Щелкнуть Tile 05, удерживая Shift |
Выбраны элементы с Tile 02 по Tile 05 |
Щелкнуть Tile 08, удерживая Command |
Tile 08 добавлен к выбору |
Щелкнуть выбранный Tile 03, удерживая Command |
Tile 03 удален из выбора |
Щелкнуть Clear selection |
Выбор пуст; в сводке указано None selected |
Выбрать Tile 04, затем обратить порядок |
Выбранный идентификатор по-прежнему соответствует Tile 04 |
Выбрать несколько строк, затем отфильтровать по 04 |
Выбранным остается только тот ранее выбранный элемент, который по-прежнему виден |
| Снять фильтр | Скрытые строки возвращаются, но удаленный выбор не восстанавливается |
Также проверьте навигацию с клавиатуры после щелчка по списку, прокрутку выбранных строк за пределы видимой области и обратно, а также начало нового диапазона после фильтрации. Для сетки AppKit добавьте проверку диапазона, проходящего через границу строки. Если результат не соответствует задаче, сначала упростите интерфейс до обычного нативного элемента управления, а затем поочередно возвращайте пользовательское содержимое строк и обработчики жестов.
Поручите нативному элементу управления обработку взаимодействий, стабильным идентификаторам — определение состава выбора, а отображаемому массиву — порядок. Если эти обязанности явно разграничены, пользовательское оформление строк, фильтрация и сортировка помогут сохранить понятность выбора.
Источники
- Выбор элементов на экране Mac — служба поддержки Apple.
- List — документация для разработчиков Apple.
- App — документация для разработчиков Apple.
- onChange(of:initial:_:) — документация для разработчиков Apple.
- Типы коллекций — «Язык программирования Swift», проект Swift.
- NSCollectionView — документация для разработчиков Apple.
- allowsMultipleSelection — документация для разработчиков Apple.
- selectionIndexPaths — документация для разработчиков Apple.
- isSelected — документация для разработчиков Apple.
- NSViewRepresentable — документация для разработчиков Apple.
- collectionView(_:shouldSelectItemsAt:) — документация для разработчиков Apple.
- collectionView(_:didSelectItemsAt:) — документация для разработчиков Apple.
- textSelection(_:) — документация для разработчиков Apple.
- selectedRanges — документация для разработчиков Apple.
- textRanges — документация для разработчиков Apple.