Чтобы исправить смещение содержимого SwiftUI за пределы окна Mac при изменении его размера, сначала выясните, выходит ли рисунок за границы макета, отказывается ли содержимое сжиматься или перестало ли встроенное представление AppKit следовать за своим контейнером. Затем исправьте именно эту границу.
Это руководство предназначено разработчикам, которые исправляют адаптивный интерфейс собственного приложения для Mac: сохраняют читаемость надписей, доступность элементов управления и возможность прокручивать длинное содержимое. В приведённой ниже диагностической последовательности используются синтетические надписи и отдельно рассматриваются макет SwiftUI, размеры окна и перенос текста AppKit.
Выясните, какая граница нарушается при изменении размера
Макет SwiftUI начинается с предложения размера от родительского представления. Дочернее представление выбирает свой размер в ответ; предложение не означает, что всё содержимое нужно обрезать по этому прямоугольнику. Apple описывает предложения минимального, максимального и идеального размеров в ProposedViewSize. Это объясняет, почему сужение окна не обязательно сужает каждое вложенное представление.
Начните с воспроизводимого теста: откройте окно приложения, сужайте его до появления первой неисправности, а затем снова расширьте. Наблюдайте за первым затронутым элементом, а не за итоговым расположением. Надпись выходит за пределы контейнера? Сдвигается вся панель? Текст сохраняет ширину переноса, которая была до изменения размера?
Временно добавьте .border(Color.orange) к подозрительному дочернему представлению, а .border(Color.blue) — к окружающему фрейму или контейнеру. Размещайте модификаторы на разных уровнях, чтобы сравнить прямоугольники. В документации Apple по offset этот приём с границами показан для различения исходного положения в макете и смещённого рисунка.
| Что вы наблюдаете | Что проверить сначала | Полезное следующее действие |
|---|---|---|
| Рисунок выходит за границы стабильного прямоугольника | offset, position, вычисления координат |
Поочерёдно удалите модификаторы позиционирования |
| Надпись остаётся широкой, хотя контейнер сужается | Горизонтальный fixedSize, явные значения ширины |
Позвольте надписи принять ограниченную ширину |
| Элементы управления не помещаются в горизонтальный ряд | Суммарная ширина элементов управления и интервалы | Предусмотрите вертикальное расположение |
| Нижняя часть содержимого исчезает при уменьшении высоты | Отсутствует поведение для переполнения | Сделайте область содержимого прокручиваемой |
| Встроенная панель SwiftUI сдвигается внутри AppKit | Границы хостинг-представления и ограничения размера | Сравните размер хоста с требованиями содержимого |
| Представление текста AppKit сужается, но строки остаются широкими | Отслеживание ширины текстового контейнера | Проверьте настройки представления текста и текстового контейнера |
Это диагностические гипотезы, основанные на документированном поведении, а не гарантия, что у конкретного симптома есть только одна причина. Меняйте по одному подозрительному элементу и повторяйте то же изменение размера. Если замена сложного дочернего представления на Text("Sample panel") устраняет неисправность, постепенно возвращайте его слои.
Удалите правила позиционирования и размера, которые мешают адаптации к окну
Проверьте смещения и позиционирование по центру
offset сдвигает отображаемое содержимое, не меняя исходных размеров представления. Поэтому положительное горизонтальное смещение может вынести рисунок за правый край, хотя родительское представление по-прежнему резервирует исходное пространство. Временно удалите смещение; если рисунок вернулся внутрь контейнера, замените эту корректировку расположением через стеки, отступы или выравнивание фрейма. См. описание поведения offset от Apple.
position(x:y:) размещает центр представления в заданных координатах системы координат родительского представления. Если считать эти координаты верхним левым углом, возникнет ошибка, связанная с размером дочернего представления. Кроме того, координаты, вычисленные один раз, могут перестать подходить при изменении размера родителя. Для обычной панели с надписью и элементами управления используйте выравнивание, которое выражает требуемое расположение относительно контейнера. Apple описывает поведение позиционирования по центру в position(x:y:).
Например, .frame(maxWidth: .infinity, alignment: .leading) задаёт выравнивание по ведущему краю внутри гибкого фрейма. Это не отменяет собственных требований к размеру чрезмерно широкого дочернего представления. В документации Apple по гибкому фрейму описано, как фрейм ограничивает предложения размера и размещает дочернее представление.
Проверьте фиксированную ширину и идеальный размер текста
Проверьте явные настройки ширины и горизонтальное использование fixedSize в проблемной ветви. Apple отдельно отмечает, что .fixedSize(horizontal: true, vertical: false) заставляет текст сохранять идеальную ширину, необходимую для всей строки, и из-за этого текст может выходить за границы родительского представления. Для переносимого основного текста удалите это горизонтальное требование. Если важно сохранить всю высоту переносимого текста, рассмотрите .fixedSize(horizontal: false, vertical: true). См. fixedSize(horizontal:vertical:).
Для сохранения вертикального размера нужно предусмотреть место, куда поместится содержимое этой высоты. Абзац, который растёт по вертикали внутри короткого непрокручиваемого контейнера, всё равно может стать недоступным. Если задача требует доступа ко всему тексту, используйте вертикальную область прокрутки; поддерживаемые оси прокрутки описаны Apple в ScrollView.
Повторно проверьте вычисления на основе геометрии
GeometryReader задаёт содержимое относительно собственного размера и системы координат, а родителю сообщает гибкий предпочтительный размер. Добавление такого элемента меняет структуру макета, поэтому это не нейтральная обёртка для измерений. Если смещение началось после добавления вычислений геометрии, временно замените эту ветвь обычными стеками и фреймами. Эти свойства описаны Apple в GeometryReader.
Если пропорциональный макет действительно требует геометрии, вычисляйте размеры по актуальному размеру соответствующего контейнера. Проверьте состояние, где хранится ширина, зафиксированная при первом появлении. Также проверьте, используется ли измеренный размер для изменения того же самого измеряемого участка; удаление такой зависимости — полезный способ изолировать проблему.
Создайте компактный макет и предусмотрите переполнение
В следующем полном примере показаны три независимых решения: текст переносится, расположение элементов управления меняется при необходимости, а содержимое, превышающее высоту области, остаётся доступным для прокрутки. Указанные размеры служат примером проектных значений и не предписаны Apple.
Создайте в Xcode приложение SwiftUI для macOS с минимальной версией macOS 13 или новее и замените сгенерированные объявления приложения и представления содержимого этим единым файлом. Оставьте только одно объявление @main. В примере используются ViewThatFits и API изменения размера окна, доступные начиная с macOS 13. Apple описывает их поведение в ViewThatFits и windowResizability(_:).
import SwiftUI
@main
struct ResizeExampleApp: App {
var body: some Scene {
WindowGroup {
ResizeExampleView()
.frame(minWidth: 280, minHeight: 220)
}
.windowResizability(.contentMinSize)
}
}
struct ResizeExampleView: View {
@State private var selection = "Sample A"
private let details = Array(
repeating: "Sample notes wrap as the panel becomes narrower.",
count: 12
).joined(separator: " ")
var body: some View {
ScrollView(.vertical) {
VStack(alignment: .leading, spacing: 16) {
Text("Sample panel")
.font(.title2)
Text(details)
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity, alignment: .leading)
ViewThatFits(in: .horizontal) {
HStack(spacing: 12) {
Button("Choose Sample A") {
selection = "Sample A"
}
Button("Choose Sample B") {
selection = "Sample B"
}
}
.fixedSize(horizontal: true, vertical: false)
VStack(alignment: .leading, spacing: 12) {
Button("Choose Sample A") {
selection = "Sample A"
}
Button("Choose Sample B") {
selection = "Sample B"
}
}
}
Text("Selected: \(selection)")
}
.padding(20)
.frame(
minWidth: 0,
maxWidth: .infinity,
alignment: .leading
)
}
}
}
ViewThatFits проверяет варианты в указанном порядке и выбирает первый, чей идеальный размер помещается по ограниченным осям. Здесь горизонтальная компоновка выбирает между рядом кнопок и вертикальным стеком. Для ряда намеренно сохраняется идеальный горизонтальный размер, чтобы сжатый ряд не стал предпочтительным вариантом. Оба варианта должны предоставлять одинаковый набор действий. См. правила выбора Apple.
Внешний фрейм позволяет колонке содержимого подстраиваться под доступную ширину, а вертикальная область прокрутки обеспечивает доступ к содержимому, когда перенос текста увеличивает высоту. Такая компоновка предполагает, что отдельные элементы управления помещаются при выбранной минимальной ширине. Для более длинных надписей или крупного шрифта может понадобиться другое минимальное значение или иная компактная компоновка.
Пример проверен на согласованность объявлений и доступность API; здесь он не компилировался, поскольку компилятор Swift был недоступен. Запустите его в macOS, чтобы проверить фактическое поведение окна и текста.
Задайте ограничения окна после исправления макета содержимого
Полезный минимальный размер окна не позволяет интерфейсу уменьшиться ниже размеров, поддерживаемых его компоновкой. С .contentMinSize SwiftUI выводит минимальный размер окна из содержимого и не задаёт максимальный размер. В примере корневой фрейм задаёт минимальные значения ширины и высоты. См. contentMinSize.
Выбирайте минимальный размер, проверяя компактную компоновку с самыми длинными поддерживаемыми надписями. Если увеличивать минимум до тех пор, пока неправильно размещённое представление не станет видно, можно замаскировать исходную ошибку макета. Сначала устраните непреднамеренное смещение и чрезмерные размеры дочерних представлений, затем задайте наименьшие размеры, при которых интерфейс остаётся удобным для нужных задач.
Если окном управляет AppKit и в нём размещён SwiftUI, отдельно проверьте границу хостинга. В macOS 13 и новее NSHostingView.sizingOptions управляет ограничениями, выводимыми из содержимого SwiftUI. Если хост является contentView окна, он также обновляет минимальный и максимальный размеры содержимого окна. Apple также указывает, что содержимое центрируется, когда фрейм хоста отличается от размера, необходимого его содержимому SwiftUI. Прежде чем добавлять компенсирующие смещения, проверьте эти взаимосвязи. См. NSHostingView sizingOptions.
Проверяйте AppKit и TextKit только для представлений, которые их используют
Позвольте SwiftUI задавать размер корневого представления
В NSViewRepresentable SwiftUI управляет frame и bounds представления AppKit, представленного в SwiftUI. Apple прямо предупреждает, что самостоятельная установка этих свойств конфликтует со SwiftUI и приводит к неопределённому поведению. Если обработчик изменения размера снова и снова задаёт фрейм корневого представления, удалите это конкурирующее управление и задавайте размер через границу SwiftUI. См. NSViewRepresentable.
Когда требуется особый расчёт размера, в macOS 13 и новее доступно sizeThatFits(_:nsView:context:). За один проход макета оно может получить несколько предложений; возврат nil запрашивает стандартный расчёт размера. Рассчитывайте результат на основе текущего предложения и содержимого, а не ширины окна при предыдущем изменении. Для неопределённого предложенного размера нужна продуманная политика измерения: не следует автоматически считать его равным нулю. См. расчёт размера представленного элемента.
Опциональная привязка в Swift позволяет проверить предложенный размер и извлечь его значение перед использованием. Это удобно при реализации политики расчёта размера, когда нужно избежать принудительного извлечения значений ширины или высоты. Поведение языка описано в The Basics: Optional Binding.
Разделите ширину переноса текста и ширину текстового представления
Если проблемное дочернее представление — NSTextView, сравните его текущую ширину с шириной текстового контейнера. Apple указывает, что NSTextContainer.widthTracksTextView по умолчанию равно false; значение true заставляет контейнер менять ширину при изменении ширины текстового представления. Именно этот параметр нужен, если строки должны переноситься с учётом ширины текстового представления. См. widthTracksTextView.
Также проверьте isHorizontallyResizable. Этот параметр определяет, меняет ли текстовое представление свою ширину, чтобы вместить текст. Это другое требование, чем перенос текста внутри заданной ширины. Для вертикально прокручиваемой области с переносимым текстом согласованно настройте обе политики и убедитесь, что содержащий макет действительно меняет размер текстового представления. См. isHorizontallyResizable.
Сохраните используемый реализацией текстовый движок. Apple указывает, что обращение к layoutManager у NSTextView или его текстового контейнера может переключить его в режим совместимости с использованием NSLayoutManager. Не добавляйте такое обращение без необходимости при диагностике представления на TextKit 2. Используйте API, подходящие для существующего движка и целевой версии системы. См. NSTextView.
Проверьте исправление при изменении ширины и высоты
Выполните эту заключительную последовательность в своём приложении:
- Медленно сужайте окно до места, где раньше возникала неисправность, затем снова расширьте его.
- Отдельно уменьшите высоту и убедитесь, что всё нужное содержимое остаётся доступным.
- Повторите проверку с длинными синтетическими надписями и многострочным текстом.
- Активируйте каждый элемент управления в широком и компактном вариантах.
- Для текстового представления AppKit проверьте, что перенос текста учитывает текущую ширину при сужении и расширении окна.
Успешное исправление привязывает расположение к текущему контейнеру, обеспечивает содержимому работоспособную компактную компоновку и даёт доступ к переполнению. Когда эти условия соблюдаются при многократном изменении размера, удалите временные границы и оставьте минимальный размер окна, обоснованный интерфейсом.
Источники
- ProposedViewSize — Apple
- offset(x:y:) — Apple
- position(x:y:) — Apple
- frame(minWidth:idealWidth:maxWidth:minHeight:idealHeight:maxHeight:alignment:) — Apple
- fixedSize(horizontal:vertical:) — Apple
- ScrollView — Apple
- GeometryReader — Apple
- ViewThatFits — Apple
- windowResizability(_:) — Apple
- contentMinSize — Apple
- sizingOptions — Apple
- NSViewRepresentable — Apple
- sizeThatFits(_:nsView:context:) — Apple
- The Basics — Swift.org
- widthTracksTextView — Apple
- isHorizontallyResizable — Apple
- NSTextView — Apple