Используйте sheet(item:onDismiss:content:) с одним необязательным выбранным элементом и формируйте содержимое листа на основе элемента, полученного замыканием. Так в приложении для Mac одно значение определяет и то, нужно ли показывать лист, и то, что он отображает.
В этом руководстве реализована небольшая задача для настольного приложения: щёлкните локальный пример элемента и сразу изучите его подробности. Здесь также приведён порядок диагностики листа, который открывается пустым, показывает предыдущий выбор или отображает устаревший текст внутри представления AppKit.
Зачем использовать лист на основе элемента?
SwiftUI предлагает два подходящих способа показа. Перегрузка с логическим значением показывает лист, когда привязка становится равной true. Перегрузка с элементом принимает необязательное идентифицируемое значение и передаёт его в замыкание содержимого. Выбирайте вариант с логическим значением для фиксированной панели, а вариант с элементом — если содержимое панели зависит от выбора. См. документацию Apple о листах на основе логического значения и листах на основе элемента.
При раздельном хранении выбора и состояния показа возможны комбинации вроде запроса на показ листа без выбранного элемента. Объединение этих значений в один необязательный элемент исключает такую комбинацию из вашей модели состояния: nil означает, что элемент не запрошен, а заполненное значение указывает элемент для просмотра. Это вывод из устройства двух API, а не утверждение о недокументированной ошибке планирования SwiftUI.
Если существующая реализация сначала присваивает выбранный элемент, а затем переключает логическое значение, сами по себе эти два оператора не объясняют, почему первый показ не срабатывает. Прежде чем связывать симптом с проблемой времени выполнения, проверьте путь данных в содержимом листа. Важно выяснить, отображает ли лист переданный ему входной параметр, читает другое свойство или показывает отдельно сохранённую копию.
Создайте полноценный пример для macOS
Создайте в Xcode проект приложения для macOS с интерфейсом SwiftUI. Для этого примера задайте целевую версию macOS 12 или новее. API листа на основе элемента доступен начиная с macOS 10.15, а действие закрытия из окружения, используемое ниже, требует macOS 12. Эти ограничения по доступности описаны Apple для листов на основе элемента и значения окружения для закрытия.
Замените сгенерированные объявление приложения и представление содержимого этой полной программой. В целевом объекте должна остаться только одна точка входа приложения. Все приведённые ниже подписи и описания — синтетические локальные примеры данных.
import SwiftUI
struct SampleItem: Identifiable {
let id: String
let title: String
let detail: String
}
@main
struct FirstSheetDemoApp: App {
var body: some Scene {
WindowGroup {
ItemListView()
}
}
}
struct ItemListView: View {
private let items = [
SampleItem(
id: "tile-a",
title: "Amber Tile",
detail: "A square sample with an amber finish."
),
SampleItem(
id: "tile-b",
title: "Blue Tile",
detail: "A rectangular sample with a blue finish."
)
]
@State private var presentedItem: SampleItem? = nil
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text("Sample Items")
.font(.title2)
ForEach(items) { item in
Button("Inspect \(item.title)") {
presentedItem = item
}
}
}
.padding(24)
.frame(minWidth: 360, minHeight: 200)
.sheet(item: $presentedItem) { item in
ItemDetailView(item: item)
}
}
}
struct ItemDetailView: View {
let item: SampleItem
@Environment(\.dismiss) private var dismiss
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text(item.title)
.font(.title)
Text(item.detail)
Text("Item ID: \(item.id)")
.font(.caption)
HStack {
Spacer()
Button("Done") {
dismiss()
}
}
}
.padding(24)
.frame(width: 380, height: 220)
}
}
Кнопка присваивает нажатое значение переменной presentedItem. Затем замыкание показа передаёт собственный параметр item в ItemDetailView. Обратите внимание: у представления подробностей есть обычное входное свойство let item, а не второе свойство состояния с выбранным элементом. Apple рекомендует хранить единственный источник истины и передавать доступные только для чтения значения через свойства, используя привязки, когда дочернему представлению нужен доступ на запись. См. State.
Действие закрытия следует объявлять внутри ItemDetailView, где оно считывает окружение листа. Apple прямо предупреждает, что считывание этого действия в представляющем представлении и его вызов из содержимого листа использует окружение представляющего представления. Как показано в примере, объявляйте действие рядом с содержимым самого листа. См. dismiss.
В этой среде программа проверена на согласованность входных данных и ссылок, но не компилировалась и не запускалась, поскольку здесь недоступен набор инструментов Swift. Выполните следующие проверки в Xcode на целевой версии macOS.
Проверьте первый показ в заданной последовательности нажатий
Перед каждой проверкой первого показа запускайте приложение заново. Проверка только после открытия другого элемента может скрыть исходный симптом. Ожидания ниже основаны на явных подписях и присваиваниях данных в примере; это критерии приёмки, а не результаты проведённых тестов.
| Действие | Ожидаемое содержимое листа | Что проверяется |
|---|---|---|
Запустите приложение и сначала нажмите Inspect Blue Tile |
Blue Tile, описание прямоугольного элемента и tile-b |
При первом показе отображается нажатый элемент |
Нажмите Done, затем откройте Amber Tile |
Amber Tile, описание квадратного элемента и tile-a |
При последующем выборе содержимое получает нужный элемент |
Закройте лист и снова откройте Amber Tile |
Те же сведения об элементе янтарного цвета | При повторном открытии данные остаются согласованными |
Перезапустите приложение и сначала откройте Amber Tile |
Сразу отображаются сведения об элементе янтарного цвета | Результат не зависит от того, какой пример открывают первым |
Проверка названия и идентификатора полезнее, чем проверка одного названия. Если в реальном интерфейсе могут встречаться одинаковые отображаемые имена, при разработке используйте два синтетических элемента с одинаковым названием, но разными идентификаторами и описаниями. Так проще обнаружить ошибочный выбор по отображаемому тексту.
Найдите причину устаревшего содержимого: от кнопки до конечного представления
Выполняйте эти проверки по порядку. Каждый шаг помогает точнее локализовать несоответствие, чтобы исправить нужный слой без посторонних обходных решений для показа.
- Проверьте входные данные действия. Установите точку останова в действии кнопки и проверьте
item.id. Убедитесь, что действие получает нужную запись. - Проверьте входные данные показа. Проверьте параметр
itemвнутри замыкания листа. Передайте этот параметр в представление подробностей, вместо того чтобы снова искать отдельный выбранный элемент. - Проверьте свойство, которое отображается в итоге. Убедитесь, что название и описание читаются из входного параметра представления подробностей. Найдите кэшированное свойство или локальный черновик со старым значением.
- Проверьте мост AppKit. Если обычный текст SwiftUI отображается правильно, а текст в обёрнутом представлении устарел, проверьте метод обновления представляемого объекта.
Сохраняйте стабильность идентичности
API листа на основе элемента требует соответствия протоколу Identifiable. Swift определяет этот протокол через стабильную идентичность, а её продолжительность и область действия оставляет типу, который ему соответствует, и его потребителю. В примере используются сохранённые строковые идентификаторы, различающие две записи. См. Identifiable.
В реальном списке выбирайте идентификатор, который продолжает указывать на ту же запись при обновлениях, поддерживаемых интерфейсом. Не используйте в качестве идентификатора название, которое может измениться или повторяться у нескольких элементов. Также не создавайте вычисляемое свойство id, каждый вызов которого возвращает новый UUID: такой идентификатор не будет стабильным.
Разделяйте входные данные для отображения и редактируемый черновик
Инспектор может напрямую отображать item.title. Редактор может намеренно создавать локальный черновик, но при этом появляется ещё одно значение, срок жизни которого нужно определить. Решите, когда он создаётся, должны ли последующие изменения модели заменять его и что происходит, когда пользователь завершает или отменяет редактирование.
Apple поясняет, что SwiftUI сохраняет хранилище состояния для идентичности представления и инициализирует его в начале срока жизни этой идентичности. Повторное создание значения представления само по себе не создаёт новое состояние. Поэтому инициализация локального состояния из входного параметра не является универсальным способом синхронизировать это состояние с последующими входными значениями. См. Demystify SwiftUI.
Для этой задачи только на чтение оставьте обычное входное свойство. Если вы добавите редактирование, используйте явную схему создания черновика и его фиксации либо привязку к нужному значению модели. Не меняйте идентичность представления только ради обновления текста: при смене идентичности заменяется связанное с ней состояние, из-за чего незавершённый черновик может быть потерян. Это следует из описанной Apple модели срока жизни состояния.
Что меняется, если лист содержит текстовое представление AppKit?
Для листа с NSTextView по-прежнему нужен тот же путь передачи данных о выбранном элементе. Кроме того, нужно правильно реализовать мост. Протокол NSViewRepresentable от Apple отделяет начальное создание в makeNSView(context:) от последующей настройки в updateNSView(_:context:). У обёртки, которая задаёт текст только при создании, нет реализации для применения последующих входных значений. См. NSViewRepresentable.
Для обёртки только на чтение инициализируйте нативное представление переданным текстом, а в методе обновления при необходимости приводите отображаемый текст в соответствие с текущим входным значением. Apple указывает, что SwiftUI вызывает метод обновления при изменениях, затрагивающих соответствующее представление AppKit. См. updateNSView(_:context:).
Для редактируемой обёртки нужна дополнительная политика: не заменяйте текст без проверки при каждом обновлении, иначе текущие правки будут потеряны. Используйте координатор, чтобы передавать изменения от делегата или механизма target-action нативного представления обратно в SwiftUI, как описано Apple в документации по представляемым объектам. Позвольте SwiftUI управлять рамкой и границами представляемого представления: Apple предостерегает от прямого изменения этих параметров компоновки.
TextKit отвечает за хранение, компоновку и отрисовку текста. Apple описывает как современный путь с NSTextLayoutManager, так и прежний путь с NSLayoutManager, доступный через NSTextView. Эти средства не передают листу выбранную запись. Сначала убедитесь, что мост получает текст нужного элемента, и лишь затем изучайте компоновку текста. Перед добавлением дополнительных API TextKit отдельно проверьте их доступность. См. TextKit.
Выберите способ показа, подходящий для задачи
Используйте перегрузку с логическим значением, если содержимое фиксировано и меняется только видимость. Для задачи инспектора подойдёт перегрузка с элементом. Apple описывает, что при изменении переданного элемента существующий лист может закрыться и замениться другим, поэтому не считайте замену элемента гарантией непрерывного редактирования в рамках того же показа. См. sheet(item:onDismiss:content:).
Чтобы первый показ был правильным, выстройте понятную цепочку передачи выбранной записи: одно присваивание в действии кнопки, один параметр элемента в замыкании листа и один входной параметр в представлении подробностей. Проверьте оба примера после нового запуска приложения. Если результат по-прежнему отличается от ожидаемого, последовательность диагностических шагов поможет определить, возникает ли несоответствие при выборе, из-за сохранённого состояния или в пути обновления нативного представления.
Источники
- sheet(item:onDismiss:content:) — Apple
- sheet(isPresented:onDismiss:content:) — Apple
- State — Apple
- dismiss — Apple
- Identifiable — Apple
- Demystify SwiftUI — Apple
- NSViewRepresentable — Apple
- updateNSView(_:context:) — Apple
- TextKit — Apple