Чтобы столбцы с метками и состояниями оставались на месте, пока описание расширяется, задайте внешним столбцам явную ширину, а описанию — гибкий размер. Используйте приоритет компоновки, когда соседние представления конкурируют за место, а идеальный размер сохраняйте только по нужным осям.
В этом руководстве создаётся небольшое окно с изменяемым размером, содержащее условные метки и описания элементов. Нужно сохранить выравнивание столбцов и при этом разрешить длинным описаниям переноситься на новые строки, когда окно становится уже. Всё относится к демонстрационному приложению; для компоновки используются обычные представления SwiftUI.
Какой способ задания размера использовать для каждого вложенного представления?
Компоновка SwiftUI начинается с того, что родитель предлагает размер. Каждый дочерний элемент отвечает предпочитаемым размером, а контейнер располагает полученные элементы. Для начала полезно определить, какие размеры каждого элемента могут меняться. Apple описывает этот процесс предложений и ответов в руководстве по компоновке в SwiftUI.
Для этого окна задайте три роли: столбец меток выбранной ширины, столбец описаний, поглощающий изменения ширины, и столбец состояний выбранной ширины. Отличайте числовую фиксированную ширину от идеального размера представления: это разные требования.
| Требование | Средство | Практический результат |
|---|---|---|
| Зарезервировать конкретную ширину столбца | .frame(width: 96) |
Получившаяся рамка имеет заданную ширину. |
| Позволить описанию занимать доступное пространство | .frame(minWidth: 160, maxWidth: .infinity, alignment: .leading) |
Рамка принимает предложенную ширину с учётом минимального значения. |
| Сохранить идеальный размер содержимого по выбранным осям | .fixedSize(horizontal: false, vertical: true) |
Ширина может меняться в зависимости от компоновки, а высота сохраняет идеальный размер. |
| Отдать предпочтение одному соседнему элементу при распределении места | .layoutPriority(1) |
Родитель отдаёт этому элементу предпочтение перед элементами с более низким приоритетом. |
Apple описывает числовые размеры в модификаторе фиксированной рамки, минимальные и максимальные ограничения — в модификаторе гибкой рамки, задание идеального размера по отдельным осям — в fixedSize(horizontal:vertical:), а предпочтение соседним элементам — в layoutPriority(_:).
Бесконечное максимальное значение разрешает расширение в пределах предложенного размера; оно не запрашивает бесконечно широкое окно. Если задать минимальное и максимальное ограничения, рамка примет предложенное измерение с учётом этих ограничений. Параметр выравнивания задаёт положение дочернего элемента внутри рамки. Эти детали важны, когда текст уже, чем отведённое его столбцу пространство. Документация Apple по гибким рамкам
Создайте полноценный пример приложения для macOS с изменяемым размером окна
Создайте в Xcode проект приложения для macOS на SwiftUI и укажите целевую версию macOS 13 или новее. Замените сгенерированные объявления приложения и представления содержимого этим файлом, оставив только одну точку входа приложения. В примере используется управление изменяемостью размера окна, доступное начиная с macOS 13. Документация Apple по WindowResizability
Ниже приведён полный пример приложения. Он включает строки для образца и переключатель между короткими и длинными описаниями. В исследовательской среде не удалось скомпилировать приложение SwiftUI, поскольку там не было набора инструментов Swift; перед интеграцией в существующий проект проверьте его поведение в Xcode.
import SwiftUI
@main
struct ColumnAllocationApp: App {
var body: some Scene {
WindowGroup {
AllocationView()
.frame(minWidth: 420, minHeight: 260)
}
.windowResizability(.contentMinSize)
}
}
struct AllocationView: View {
@State private var showLongDescriptions = true
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text("Item overview")
.font(.title2)
Toggle(
"Show longer descriptions",
isOn: $showLongDescriptions
)
ScrollView {
VStack(alignment: .leading, spacing: 16) {
AllocationRow(
label: "Item A",
summary: showLongDescriptions
? "A blue folder with several sample notes and a longer descriptive label."
: "Blue folder",
status: "Ready"
)
AllocationRow(
label: "Item B",
summary: showLongDescriptions
? "A green notebook containing a short list of synthetic example entries."
: "Green notebook",
status: "Draft"
)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.padding(16)
}
}
struct AllocationRow: View {
let label: String
let summary: String
let status: String
var body: some View {
HStack(alignment: .top, spacing: 12) {
Text(label)
.frame(width: 96, alignment: .leading)
Text(summary)
.fixedSize(horizontal: false, vertical: true)
.frame(
minWidth: 160,
maxWidth: .infinity,
alignment: .leading
)
Text(status)
.fixedSize(horizontal: true, vertical: false)
.frame(width: 72, alignment: .trailing)
}
}
}
Описание — единственный столбец с гибкой шириной. Вызов fixedSize оставляет горизонтальный размер адаптивным, сохраняя вертикальное пространство, необходимое тексту с переносами. Затем гибкая рамка заполняет отведённый столбцу размер. Состояние сохраняет идеальную ширину текста внутри отдельной рамки шириной 72 пункта. Apple предупреждает, что содержимое с идеальным размером может выходить за границы родителя, поэтому такой способ отображения состояния рассчитан на короткие значения Ready и Draft. Документация Apple по fixedSize
Высота строк не фиксирована. Это сделано намеренно: более узкие описания могут занимать больше строк. Прокручиваемое представление позволяет добраться до строк, когда их суммарная высота превышает доступное пространство. Не добавляйте слишком строгую фиксированную высоту строки только ради одинакового вида при первоначальном расположении.
В окне используется .contentMinSize: он рассчитывает минимальные размеры по содержимому и не задаёт максимальный размер. В этом примере минимальная ширина корневого представления в 420 пунктов задаёт практическую нижнюю границу. Этот параметр применяется к окнам, создаваемым сценой приложения. Документация Apple по contentMinSize
Объявления с возвращаемым типом some View используют непрозрачные типы результата Swift: конкретные типы скрыты, но соответствие протоколу View остаётся доступным. Цепочки модификаторов можно менять, не указывая вложенные типы возвращаемых значений. Язык программирования Swift: непрозрачные и упакованные типы протоколов
Рассчитайте доступную ширину, прежде чем менять приоритеты
Расчёт ширины явно показывает ограничения компоновки. В этом примере из ширины содержимого окна вычитаются горизонтальные отступы, оба промежутка и два столбца фиксированной ширины. Остаток отводится описанию. Это примерные размеры для дизайна, а не измерения стандартных элементов управления.
Ширина описания
= ширина содержимого - горизонтальные отступы - промежутки - столбцы фиксированной ширины
= ширина содержимого - (2 × 16) - (2 × 12) - 96 - 72
= ширина содержимого - 224
При ширине 640 пунктов для описания остаётся 416 пунктов.
При ширине 420 пунктов для описания остаётся 196 пунктов.
Минимальная ширина строки:
32 + 24 + 96 + 160 + 72 = 384 пункта.
Арифметика проверена с помощью детерминированного локального расчёта. Она описывает строку при ширине содержимого за вычетом указанных отступов; оформление окна не учитывается. Минимальная ширина окна в 420 пунктов оставляет запас относительно рассчитанной минимальной ширины строки в 384 пункта. Если внешний контейнер добавляет внутренние отступы, включите их в расчёт.
Этот расчёт также помогает выявить невыполнимое требование. Если ширина меньше суммарного минимального значения, все три столбца не смогут одновременно сохранить ширину и промежутки. Выберите большую минимальную ширину окна, меньшие минимальные размеры или другой способ расположения. Повышение приоритета не создаёт дополнительного пространства.
Когда помогает приоритет компоновки?
Приоритет полезен, когда два соседних элемента могут сжиматься и вы хотите, чтобы один из них сохранял больше места. Например, описательный заголовок может быть важнее дополнительного пояснения. Добавьте .layoutPriority(1) дочернему элементу, участвующему в решении родителя о распределении места. Apple указывает, что представления с более высоким приоритетом сжимаются позже и растягиваются раньше, а минимальные требования дочерних элементов с более низким приоритетом сохраняются. Документация Apple по layoutPriority
В полном примере модификатор приоритета не нужен: правила распределения уже заданы двумя столбцами фиксированной ширины и одним гибким столбцом. Значение приоритета также не является пропорциональным коэффициентом. Приоритеты 2 и 1 не задают соотношение ширины два к одному; согласно документации, приоритет определяет предпочтение при распределении пространства. Документация Apple по layoutPriority
Не добавляйте горизонтальный Spacer между описанием и состоянием, если вам не нужен отдельный гибкий промежуток. Заполнитель расширяется вдоль главной оси стека и раздвигает соседние элементы. Поскольку рамка описания уже поглощает изменения ширины, здесь такой дополнительный элемент не нужен. Документация Apple по Spacer
Что делать, когда текст перестаёт помещаться?
Отдельно решите, должен ли каждый текстовый элемент переноситься, обрезаться или требовать другого расположения. Описания в примере могут переноситься без ограничения числа строк. Применение .lineLimit(1) ограничит каждый затронутый текстовый элемент одной строкой и изменит требование. Значение ограничения строк nil снимает этот предел, но не отменяет ограничений внешних рамок. Документация Apple по lineLimit
Для более длинных значений состояния пересмотрите ширину столбца в 72 пункта. Уберите сохранение идеального размера по горизонтали, если перенос допустим, увеличьте ширину столбца или расположите состояние под описанием. Если оставить .fixedSize(horizontal: true, vertical: false) и сузить окружающую рамку, текст может выходить за её пределы. Документация Apple по fixedSize
Если компактное окно должно менять расположение содержимого, в macOS 13 и новее доступен ViewThatFits. Сначала укажите горизонтальный вариант расположения, а затем вариант со столбцами. Представление выбирает первый дочерний элемент, чей идеальный размер помещается по ограниченным осям, поэтому при решении, определяющим фактором которого является ширина, проверяйте горизонтальное пространство с помощью ViewThatFits(in: .horizontal). Последний вариант должен оставаться пригодным для минимальной поддерживаемой ширины. Документация Apple по ViewThatFits
Рассматривайте это как альтернативное правило компоновки. В текущем примере столбцы сохраняются намеренно, а для окна задаётся минимальная ширина. Используйте запасной вариант со столбцами, если возможность сделать окно уже важнее сохранения горизонтального расположения.
Как встроенные представления AppKit и TextKit меняют подход?
Если позднее описание станет текстовым компонентом AppKit, интегрируйте представление с помощью NSViewRepresentable. Apple прямо указывает, что SwiftUI управляет свойствами frame и bounds представленного представления; прямое изменение этих свойств в собственном коде конфликтует с компоновкой SwiftUI. Настраивайте содержимое через жизненный цикл представления-обёртки, а внешний размер задавайте средствами SwiftUI. Документация Apple по NSViewRepresentable
Начиная с macOS 13 метод sizeThatFits(_:nsView:context:) представления-обёртки может возвращать предпочтительный размер для предложенного значения. В течение одного прохода компоновки его могут вызвать несколько раз с разными предложениями. Возврат nil запрашивает алгоритм задания размера по умолчанию. При измерении компонента с переносами учитывайте предложенную ширину, а не возвращайте всегда ширину текста без переносов. Документация Apple по заданию размера представления-обёртки
Отличайте неуказанный размер от нулевого: SwiftUI использует неуказанный размер, чтобы запросить идеальный размер, а нулевой — чтобы проверить минимальный размер. Пользовательская процедура измерения должна специально обрабатывать оба случая. Документация Apple по ProposedViewSize
Внутри компоновки AppKit сопротивление сжатию определяет, насколько сильно NSView не позволяет уменьшить себя относительно внутреннего размера. Если компонент с переносами сопротивляется сжатию, проверьте этот параметр наряду с поведением задания размера в SwiftUI. Документация Apple по setContentCompressionResistancePriority
Для NSTextView с TextKit 2 используйте современный путь textLayoutManager. В актуальном обзоре TextKit от Apple он отличается от устаревшего layoutManager; в руководстве по миграции поясняется, что обращение к устаревшему свойству может переключить этот экземпляр на TextKit 1 без возможности вернуться обратно. Для распределения места между соседними столбцами менять текстовый движок не требуется. Обзор TextKit от Apple, Новое в TextKit и текстовых представлениях
Проверьте результат при минимальном и максимальном размере окна
Запустите пример и проверьте следующие условия, прежде чем применять этот подход к более крупному интерфейсу:
- Расширьте окно: столбец описания должен расшириться, а рамки меток и состояний — сохранить свою ширину.
- Сузьте окно до поддерживаемого минимума: длинные описания должны переноситься и увеличивать высоту строк.
- Выключите и снова включите параметр
Show longer descriptions: короткие описания должны оставаться выровненными по тем же столбцам. - Подставьте более длинное условное значение состояния: решите, следует ли расширить это поле, разрешить переносы или изменить его расположение.
- Уменьшите доступную высоту: убедитесь, что до всех строк по-прежнему можно добраться.
Чтобы увидеть, какой уровень получает пространство, временно добавьте рамки непосредственно перед гибкой рамкой описания и после неё. Внутренняя рамка показывает границы текста, а внешняя — выделенный столбцу размер. Apple рекомендует цветные рамки как практический способ диагностики размеров стеков и внутренних отступов. Руководство по компоновке в SwiftUI
По мере расширения интерфейса задавайте правила размеров явно: резервируйте ширину там, где нужно выравнивание, отводите переменному содержимому определённую гибкую область и решайте, как каждое поле ведёт себя при минимальной ширине. С такими правилами и расчётом ширины можно менять метки или добавлять содержимое, не гадая, какому модификатору нужен более высокий приоритет.
Источники
- Руководство по компоновке в SwiftUI — Apple
- frame(width:height:alignment:) — Apple
- frame(minWidth:idealWidth:maxWidth:minHeight:idealHeight:maxHeight:alignment:) — Apple
- fixedSize(horizontal:vertical:) — Apple
- layoutPriority(_:) — Apple
- WindowResizability — Apple
- contentMinSize — Apple
- Непрозрачные и упакованные типы протоколов — Swift.org
- Spacer — Apple
- lineLimit(_:) — Apple
- ViewThatFits — Apple
- NSViewRepresentable — Apple
- sizeThatFits(_:nsView:context:) — Apple
- ProposedViewSize — Apple
- setContentCompressionResistancePriority(_:for:) — Apple
- TextKit — Apple
- Новое в TextKit и текстовых представлениях — Apple