Чтобы совместить боковые элементы управления с предварительным просмотром SwiftUI с фиксированным соотношением сторон, рассчитайте размер предварительного просмотра, подогнанного под доступное пространство окна, затем задайте элементам управления ту же высоту. В этом примере для macOS синтетический предварительный просмотр сохраняет пропорциональность, а переполняющие элементы управления становятся прокручиваемыми.
Задача — создать редактор для настольных компьютеров с изменяемым размером, где слева находится предварительный просмотр, а справа — панель элементов управления фиксированной ширины. Здесь «предварительный просмотр» означает контент внутри вашего запущенного приложения. Цель — выровнять верхние и нижние края макета обеих панелей при изменении размера окна.
Какую высоту должны следовать элементы управления?
Начните с определения прямоугольника, который вы хотите совместить. Предварительный просмотр, подогнанный по соотношению сторон, может занимать меньше места, чем его окружающий контейнер. Если элементы управления следуют за высотой контейнера, они могут выходить за пределы видимого предварительного просмотра, даже если оба представления правильно размещены.
Apple определяет соотношение сторон как ширину, деленную на высоту. Для контента с изменяемым размером aspectRatio(_:contentMode:) с .fit сохраняет это соотношение, вписывая контент в предлагаемое пространство. Пример с эллипсом в документации показывает, почему подогнанная форма и охватывающий её квадратный фрейм могут иметь разные размеры. Документация Apple по соотношению сторон.
Эта реализация делает подогнанный прямоугольник явным. Она резервирует фиксированную ширину для элементов управления и зазор между панелями, затем вписывает предварительный просмотр в оставшуюся ширину и доступную высоту. Обе панели получают рассчитанную высоту предварительного просмотра.
Это политика макета для данного примера: предварительный просмотр определяет высоту области просмотра элементов управления. Длинный список элементов управления прокручивается внутри этой области, вместо того чтобы увеличивать высоту предварительного просмотра. Apple описывает ScrollView как отображающую подвижную часть своего базового контента в прокручиваемой области. Документация Apple по ScrollView.
Вычислить подогнанный прямоугольник один раз
Пусть W и H — доступные размеры контента после внешних отступов, C — ширина элементов управления, G — зазор, а R — соотношение ширины предварительного просмотра к его высоте. Расчёт выглядит так:
remainingWidth = max(0, W - C - G)
previewWidth = min(remainingWidth, H * R)
previewHeight = previewWidth / R
controlsHeight = previewHeight
Первое ограничение предотвращает использование предварительным просмотром горизонтального пространства элементов управления. Второе предотвращает создание широким окном предварительного просмотра, более высокого, чем доступная область. Вычисление высоты из выбранной ширины сохраняет соотношение в любом случае.
Например, предположим, что область контента до отступов составляет 960 на 600 пунктов. С отступом 16 пунктов по каждому краю, панелью элементов управления шириной 220 пунктов, зазором 16 пунктов и предварительным просмотром 16:9 доступная область составляет 928 на 568 пунктов. Оставшаяся ширина предварительного просмотра — 692 пункта, что даёт высоту предварительного просмотра 389,25 пункта. Область просмотра элементов управления, следовательно, становится 220 на 389,25 пунктов.
Это иллюстративные входные данные, а не измеренные результаты приложения. Они также показывают, почему совмещение элементов управления с полной доступной высотой 568 пунктов привело бы к неверному выравниванию.
Используйте CGFloat для размеров макета и положительное, конечное соотношение. В Swift let ratio: CGFloat = 16.0 / 9.0 делает предполагаемое вычисление с плавающей точкой явным. Если ваше приложение позже будет принимать настраиваемые соотношения, проверяйте их перед делением.
Создать полный пример для macOS
Создайте приложение macOS SwiftUI в Xcode и установите цель развёртывания на macOS 13 или новее. Замените сгенерированные объявления приложения и представления контента этим единственным файлом, оставив ровно одно объявление @main в цели. Пример не требует ресурсов или внешних пакетов.
Требование к развёртыванию вызвано используемым здесь API изменения размера окна. Сцена использует .contentMinSize, который, как документирует Apple, выводит минимальный размер окна из его контента, не накладывая максимального размера. Документация Apple по изменяемости размера окна, Документация Apple по минимальному размеру контента.
import SwiftUI
@main
struct MatchedPreviewApp: App {
var body: some Scene {
WindowGroup {
PreviewEditor()
.frame(minWidth: 640, minHeight: 360)
}
.windowResizability(.contentMinSize)
}
}
struct PreviewEditor: View {
@State private var intensity: Double = 0.5
@State private var showsLabel = true
private let ratio: CGFloat = 16.0 / 9.0
private let controlsWidth: CGFloat = 220
private let gap: CGFloat = 16
var body: some View {
GeometryReader { geometry in
let availableHeight = max(0, geometry.size.height)
let remainingWidth = max(
0,
geometry.size.width - controlsWidth - gap
)
let previewWidth = min(
remainingWidth,
availableHeight * ratio
)
let previewHeight = previewWidth / ratio
HStack(alignment: .top, spacing: gap) {
RoundedRectangle(cornerRadius: 12)
.fill(Color.blue.opacity(0.2 + intensity * 0.7))
.aspectRatio(ratio, contentMode: .fit)
.overlay {
Text("Sample composition")
.font(.headline)
.opacity(showsLabel ? 1 : 0)
}
.frame(
width: previewWidth,
height: previewHeight
)
ScrollView(.vertical) {
VStack(alignment: .leading, spacing: 16) {
Text("Preview controls")
.font(.headline)
Toggle("Show label", isOn: $showsLabel)
VStack(alignment: .leading, spacing: 8) {
Text("Color intensity")
Slider(value: $intensity, in: 0...1)
}
ForEach(1...8, id: \.self) { index in
Text("Sample setting \(index)")
.font(.caption)
}
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .leading)
}
.frame(
width: controlsWidth,
height: previewHeight
)
.background(Color.gray.opacity(0.08))
}
.frame(
width: geometry.size.width,
height: geometry.size.height,
alignment: .center
)
}
.padding(16)
}
}
GeometryReader предоставляет размер региона, выделенного редактору. Apple описывает его как определяющий контент из своего собственного размера и координатного пространства, с гибким предпочтительным размером. Размещение отступа снаружи ридера означает, что расчёт использует внутреннюю область контента. Документация Apple по GeometryReader.
Два фиксированных фрейма устанавливают общую высоту. Apple документирует frame(width:height:alignment:) как создание невидимого фрейма с указанными размерами. Фон элементов управления следует после этого фрейма, поэтому он покрывает область просмотра элементов управления, включая любое пространство под коротким списком. Документация Apple по фиксированному фрейму.
Итоговый фрейм центрирует полный ряд внутри ридера. Он не увеличивает ни одну из панелей. Следовательно, высокое окно может содержать неиспользуемое пространство сверху и снизу ряда, пока элементы управления всё ещё совпадают с предварительным просмотром.
Сохранить состояние и поведение жизненного цикла при изменении размера
Храните редактируемые пользователем значения отдельно от вычисляемой геометрии. В примере intensity и showsLabel — это состояние; ширина и высота предварительного просмотра — локальные значения, производные от текущего доступного размера.
Apple описывает состояние как хранилище, управляемое SwiftUI, изменения которого обновляют зависящие части иерархии представлений. Это поддерживает хранение значений ползунка и переключателя в редакторе, который их владеет. Документация Apple по State.
Расчёт макета не содержит назначений состояния, обратных вызовов внешнего вида, асинхронной работы или обратной связи измерений. Изменение размера меняет размеры, используемые той же структурой представления. Метка остаётся присутствующей и меняет непрозрачность при изменении переключателя.
При адаптации этого к существующему редактору сохраняйте его владение моделью и типы привязок. Применяйте политику размеров вокруг предварительного просмотра и элементов управления; не переносите создание модели в расчёт геометрии. Арифметика этого примера требует только доступных размеров и выбранного соотношения.
Проверить изменение размера, переполнение и порядок модификаторов
Используйте следующие размеры как проверки расчёта. Они описывают область контента до внешних отступов примера, а не внешний фрейм окна. Ожидаемые значения округлены до двух знаков после запятой.
| Область контента | Размер предварительного просмотра | Область просмотра элементов управления |
|---|---|---|
| 960 × 600 пунктов | 692 × 389,25 | 220 × 389,25 |
| 640 × 360 пунктов | 372 × 209,25 | 220 × 209,25 |
| 1200 × 400 пунктов | 654,22 × 368 | 220 × 368 |
| 700 × 900 пунктов | 432 × 243 | 220 × 243 |
Арифметика проверялась с небольшим расчётом на Python, включая утверждения, что подогнанный предварительный просмотр остаётся в пределах доступной высоты и что обе панели плюс зазор помещаются по горизонтали. Swift и Xcode были недоступны в исследовательской среде, поэтому полная программа SwiftUI была просмотрена, но не компилировалась и не запускалась.
В Xcode запустите приложение и измените его размер через широкие, низкие и высокие формы. Подтвердите, что края панелей остаются выровненными, что элементы управления прокручиваются, когда их содержимое превышает область просмотра, и что ползунок и переключатель сохраняют свои значения при изменении размера.
Если выравнивание всё ещё отличается после адаптации примера, проверьте эти конкретные условия:
- Лишний отступ панели: Отступ, применённый снаружи только одного фиксированного фрейма панели, увеличивает общий отпечаток этой панели. Храните отступ элементов управления внутри прокручиваемого представления, как показано.
- Большая обёртка предварительного просмотра: Если вы добавляете более крупный фрейм вокруг предварительного просмотра, решите, должны ли элементы управления совпадать с этой обёрткой или с подогнанным контентом. Расчёт здесь совпадает с подогнанным прямоугольником.
- Переполнение пользовательского контента: Фрейм устанавливает размеры макета, но не автоматически скрывает отрисовку за их пределами. Apple документирует
.clipped()для прямоугольной обрезки; используйте её, когда намеренное переполнение предварительного просмотра должно быть скрыто. Держите элементы управления прокручиваемыми, чтобы все настройки оставались доступными. Документация Apple по обрезке. - Недостаточное пространство хоста: Пример полагается на свой минимальный размер контента. Если он встроен в более мелкого хоста, его фиксированная ширина элементов управления может не оставить полезной ширины предварительного просмотра. Выберите более мелкую панель, вертикальное расположение или явный минимум для этого хоста.
Когда следует использовать пользовательский макет?
Этот подход подходит для редактора, который получает ограниченный регион окна и использует известное соотношение сторон. Его политика размеров явна и не зависит от измерения внутреннего контента предварительного просмотра.
Если ваш многократно используемый компонент должен определять свой предпочтительный размер из своих дочерних элементов, рассмотрите пользовательский Layout. Документированный контракт Apple разделяет сообщение о составном размере через sizeThatFits(proposal:subviews:cache:) от позиционирования дочерних элементов через placeSubviews(in:proposal:subviews:cache:). Прокси подпредставлений предоставляют операции измерения и размещения. Документация Apple по Layout.
Для ограниченного настольного редактора, показанного здесь, достаточно общего подогнанного прямоугольника: зарезервируйте горизонтальное пространство для элементов управления, впишите предварительный просмотр в обе доступные размеры и назначьте его высоту области просмотра элементов управления. Это даёт вам чёткое правило изменения размера, оставляя контент элементов управления свободным для прокрутки.
Источники
- aspectRatio(_:contentMode:) — Документация разработчика Apple.
- ScrollView — Документация разработчика Apple.
- windowResizability(_:) — Документация разработчика Apple.
- contentMinSize — Документация разработчика Apple.
- GeometryReader — Документация разработчика Apple.
- frame(width:height:alignment:) — Документация разработчика Apple.
- State — Документация разработчика Apple.
- clipped(antialiased:) — Документация разработчика Apple.
- Layout — Документация разработчика Apple.