Чтобы создавать интерактивные превью SwiftUI, задайте каждому превью собственное локальное состояние, передайте редактору наблюдаемую модель представления и предоставьте редактируемые свойства через привязки. В этом руководстве создаётся небольшой редактор меток для macOS с искусственными примерами текста.

Выберите, где будет храниться состояние превью

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

Используйте минимальный набор входных данных, необходимый для взаимодействия. Apple рекомендует передавать представлениям только нужные им данные, поскольку сложные зависимости затрудняют создание превью. Для статической метки подойдёт строка; редактору нужна доступная для записи связь с состоянием. Добавление превью в файлы интерфейса.

Что нужно представлению Подходящий входной параметр Где хранить состояние примера
Отображать фиксированный текст Неизменяемая строка Непосредственно в превью
Изменять одно значение, принадлежащее родительскому представлению Свойство @Binding Локальное @State в контейнере превью
Изменять несколько связанных наблюдаемых свойств Наблюдаемая модель, предоставленная через @Bindable Экземпляр модели в @State контейнера превью
Читать модель, переданную через окружение Зависимость из окружения Явная передача в корень превью

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

Подготовьте целевой проект SwiftUI для macOS

Для приведённого здесь синтаксиса @Previewable используйте Xcode 16 или новее. Apple добавила этот макрос в Xcode 16. Установите для примера целевую версию macOS 14 или новее, где SwiftUI поддерживает Observation; в актуальной справке Apple по Previewable() также указана поддержка macOS 14. Примечания к выпуску Xcode 16, Управление данными модели в приложении, Previewable().

Добавьте в существующий целевой проект приложения SwiftUI для macOS файл Swift с именем LabelEditor.swift. Следующие два блока кода вместе образуют полный пример превью. Оставьте существующую точку входа приложения; в этом файле определяются модель, редактор и превью.

В коде используется написание @State, поддерживаемое разными версиями инструментов. В актуальной документации Apple описано различие в реализации: в Xcode 26 и более ранних версиях используется обёртка свойства State, а в Xcode 27 и новее — макрос State(). Учитывайте это при просмотре сгенерированного кода и анализе затрат на инициализацию. State, State().

Создайте наблюдаемую модель и редактируемое представление

Примените к модели @Observable, чтобы SwiftUI отслеживал свойства, которые читает интерфейс. Используйте в редакторе @Bindable, чтобы создать привязки свойств для текстовых полей и переключателя. Apple описывает такое сочетание для редактирования наблюдаемых моделей. Управление данными модели в приложении, Bindable.

import Foundation
import Observation
import SwiftUI

@MainActor
@Observable
final class LabelViewModel {
    var title: String
    var detail: String
    var showsDetail: Bool

    init(
        title: String,
        detail: String,
        showsDetail: Bool = true
    ) {
        self.title = title
        self.detail = detail
        self.showsDetail = showsDetail
    }

    var displayedTitle: String {
        let trimmed = title.trimmingCharacters(
            in: .whitespacesAndNewlines
        )
        return trimmed.isEmpty ? "Untitled label" : trimmed
    }
}

@MainActor
struct LabelEditor: View {
    @Bindable var model: LabelViewModel

    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            VStack(alignment: .leading, spacing: 6) {
                Text("Title")
                TextField("Label title", text: $model.title)
            }

            VStack(alignment: .leading, spacing: 6) {
                Text("Detail")
                TextField("Label detail", text: $model.detail)
            }

            Toggle("Show detail", isOn: $model.showsDetail)

            Divider()

            Text("Rendered label")
                .font(.caption)
                .foregroundStyle(.secondary)

            VStack(alignment: .leading, spacing: 8) {
                Text(model.displayedTitle)
                    .font(.title2)
                    .fixedSize(horizontal: false, vertical: true)

                if model.showsDetail {
                    Text(model.detail)
                        .foregroundStyle(.secondary)
                        .fixedSize(horizontal: false, vertical: true)
                }
            }
            .frame(maxWidth: .infinity, alignment: .leading)
            .padding()
            .background(
                .quaternary,
                in: RoundedRectangle(cornerRadius: 10)
            )
        }
        .textFieldStyle(.roundedBorder)
        .padding(20)
    }
}

Модель намеренно принимает все значения примера через инициализатор. Инициализатор только присваивает значения локальным свойствам. Резервный вариант меняет отображаемый текст метки, не перезаписывая введённый пользователем заголовок: заголовок, состоящий только из пробелов, остаётся редактируемым, а в результате отображается Untitled label.

Редактор получает модель от вызывающего кода. Объявление @Bindable предоставляет привязки, например $model.title; состояние будет принадлежать контейнеру превью. Если бы это представление только показывало значения модели, для наблюдения хватило бы обычной ссылки на модель. Здесь хранилище Bindable полезно, поскольку элементам управления нужны доступные для записи привязки к свойствам. Bindable.

Задайте каждому превью независимое состояние примера

Добавьте следующие объявления в тот же файл. Каждое создаёт собственную модель с явно заданными искусственными значениями. Ширина служит для наглядной проверки компоновки, а не задаёт рекомендуемый минимальный размер окна.

#Preview("Standard label") {
    @Previewable @State var model = LabelViewModel(
        title: "Sample tile",
        detail: "Blue square, medium size"
    )

    LabelEditor(model: model)
        .frame(width: 420)
        .preferredColorScheme(.light)
}

#Preview("Long label in a narrow editor") {
    @Previewable @State var model = LabelViewModel(
        title: "Sample collection with a longer descriptive label",
        detail: "Round tiles arranged in three columns"
    )

    LabelEditor(model: model)
        .frame(width: 300)
        .preferredColorScheme(.dark)
}

#Preview("Blank title and hidden detail") {
    @Previewable @State var model = LabelViewModel(
        title: "   ",
        detail: "An optional detail line",
        showsDetail: false
    )

    LabelEditor(model: model)
        .frame(width: 420)
}

@Previewable включает объявление локального состояния во встроенное представление SwiftUI, создаваемое #Preview. Согласно документации Apple, такие помеченные объявления становятся свойствами этого сгенерированного представления. Размещайте объявление в корне замыкания превью; использование @Previewable вне тела превью вызовет ошибку. Previewable().

В результате цепочка владения задана явно: сгенерированный контейнер превью хранит модель в @State, LabelEditor получает ссылку на модель, а @Bindable предоставляет её редактируемые свойства. SwiftUI поддерживает хранение модели Observation в состоянии и передачу этой ссылки дочерним представлениям. State.

Рассматривайте значения инициализатора как исходные данные примера. Интерактивное превью может менять их во время работы. Чтобы воспроизвести определённую компоновку, вернитесь к новому экземпляру превью и сравните исходный вид.

Проверьте поведение с помощью небольшой матрицы сценариев

Откройте LabelEditor.swift, выберите Show Canvas и выберите именованное превью на вкладках холста. Если холст приостановлен, нажмите Restart. Включите режим Live, чтобы проверить ввод текста и логику элементов управления. Эти элементы управления описаны в актуальном руководстве Apple по холсту. Взаимодействие с превью на холсте.

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

Сценарий Действие Ожидаемый результат
Стандартная метка Замените заголовок на Amber circle Отображаемый заголовок сразу меняется
Видимость подробностей Выключите, а затем включите Show detail Подробности исчезают и появляются снова с прежним текстом
Пустой заголовок Удалите все символы заголовка Вместо заголовка отображается Untitled label
Заголовок из пробелов Введите только пробелы Поле сохраняет пробелы, а в заголовке используется резервный вариант
Узкий редактор Просмотрите превью с длинным заголовком Отображаемый заголовок переносится в пределах карточки
Отдельные данные примеров Измените одно превью, затем откройте другое, ещё не изменённое В другом превью отображаются собственные объявленные значения модели

Если ввод текста работает, но отображаемый результат не меняется, проверьте связь между привязкой поля и свойством, которое читает результат. Здесь оба пути ведут к model.title — напрямую или через displayedTitle. Если при переключении видимости подробности очищаются, проверьте, нет ли кода, который присваивает пустое значение; в этом примере меняется только showsDetail.

В исследовательской среде не было компилятора Swift и среды выполнения превью macOS, поэтому примеры сверили с документированными API, но не компилировали и не запускали. Перед адаптацией редактора соберите целевой проект и выполните проверки взаимодействия в Xcode.

Отделяйте общую настройку от редактируемых данных примеров

Для такой небольшой модели локальное создание упрощает понимание каждого превью. Если нескольким превью понадобится дорогостоящая повторно используемая настройка, Apple предоставляет PreviewModifier: общий контекст кэшируется и повторно используется всеми задействованными превью. Для API требуется macOS 15 или новее. PreviewModifier.

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

Если существующее представление получает модель из окружения по типу, внедрите требуемый экземпляр в корень превью. Согласно документации Apple, отсутствие необязательной модели запрошенного типа вызывает исключение. Поэтому превью должно предоставлять зависимости, которые читает выбранное корневое представление. Управление данными модели в приложении.

Сохраните границы AppKit и TextKit

В приведённом выше редакторе используются элементы управления SwiftUI. Если в существующем редакторе для macOS есть текстовое представление AppKit, используйте NSViewRepresentable, чтобы встроить его в иерархию SwiftUI. Создайте и настройте его через makeNSView, а затем примените поступившее состояние SwiftUI через updateNSView. Передавайте изменения делегата AppKit через координатор, если нужно возвращать правки в SwiftUI. Пусть SwiftUI управляет рамкой и границами представленного представления. NSViewRepresentable.

Apple также поддерживает прямой просмотр представлений и контроллеров представлений AppKit. Однако @Previewable доступен только в SwiftUI и неприменим к прямым превью AppKit. Чтобы использовать описанный в этой статье шаблон локального состояния с содержимым AppKit, просматривайте контейнер SwiftUI, в который встроено представляемое представление. Добавление превью в файлы интерфейса.

При создании NSTextView сохраняйте выбранный текстовый движок. В документации Apple указано, что NSTextView(usingTextLayoutManager: true) позволяет выбрать TextKit 2 при инициализации. Руководство Apple по TextKit также поясняет, что обращение к layoutManager из TextKit 1 может вызвать необратимый переход на другой движок для этого экземпляра текстового представления. Если вы намерены использовать TextKit 2, применяйте соответствующие API; превью не отменяет этих ограничений. Новое в TextKit и текстовых представлениях.

Диагностируйте ошибки превью на границе владения

Начните с первого диагностического сообщения при сборке. Макросы Swift генерируют код во время компиляции, поэтому ошибка, отмеченная в месте вызова макроса, может быть связана со сгенерированными объявлениями. Команда Xcode Editor > Expand Macro позволяет просмотреть этот код. Применение макросов.

Затем проверьте механизм состояния. Для существующего объекта ObservableObject на основе Combine нужен соответствующий шаблон владения: Apple предупреждает, что хранение такого объекта в State не отслеживает изменения его опубликованных свойств, и рекомендует владельцам использовать StateObject. В этом примере применяется @Observable, поэтому состояние управляется по схеме Observation. State.

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

Источники