Чтобы измерить метку в приложении для Mac, не растягивая её, добавьте onGeometryChange к представлению, которое хотите проверить. Для более старой целевой версии поместите GeometryReader на задний план. Положение модификатора определяет, какую ширину вы измеряете.
В этом руководстве измеряется синтетическая метка вместе с отступами, при этом окружающий её макет не зависит от результата. Также объясняется, как отличить текущую ширину метки от её идеальной ширины и ширины контейнера.
Почему geometry reader заставляет макет расширяться?
Макет SwiftUI строится на предложениях и ответах: родитель предлагает пространство, а дочерние представления сообщают вычисленные размеры. Добавление рамки вводит в эту иерархию ещё одно представление, поэтому метка и её рамка могут иметь разную ширину. Apple объясняет этот процесс в статье Размещение простого представления.
GeometryReader — это контейнер с гибким предпочтительным размером. Поэтому, если обернуть компактную метку в него, для родителя изменится участник макета. Вы измеряете контейнер reader, который может занимать намного больше места, чем требуется тексту. См. документацию Apple по GeometryReader.
Для наблюдения добавьте измерение к существующему представлению. Если нужен reader на заднем плане, важно его расположение: Apple указывает, что фон получает размер представления, к которому он добавлен. Благодаря этому область измерения определяется меткой. См. Добавление фона к представлению.
Прежде чем выбирать способ реализации, решите, на какой вопрос должен отвечать интерфейс:
| Ваш вопрос | Подходящий способ |
|---|---|
| Какова ширина этой метки в текущем макете? | Наблюдать за геометрией метки. |
| Какова ширина плашки с отступами вокруг метки? | Наблюдать после добавления отступов. |
| Какова ширина строки, содержащей метку? | Наблюдать за строкой или её рамкой. |
| Какую ширину предпочёл бы весь текст? | Отдельно исследовать идеальный размер. |
| Какой из нескольких вариантов отображения помещается? | Рассмотреть ViewThatFits. |
| Как измерить и разместить вместе несколько дочерних представлений? | Рассмотреть пользовательский Layout. |
Это различие помогает избежать распространённой ошибки: ширину ограниченной, возможно усечённой метки принимают за пространство, необходимое для отображения всей строки целиком.
Измерение текущей ширины в macOS 13 и новее
В актуальной документации Apple однопараметрический вариант onGeometryChange(for:of:action:) с обработчиком указан как доступный в macOS 13 и новее. Он преобразует прокси геометрии в эквивалентное значение и вызывает обработчик при изменении этого значения. Используйте SDK, в который входит этот API, и оставьте показанный здесь обработчик с одним параметром. См. onGeometryChange(for:of:action:).
Создайте в Xcode проект приложения для macOS на SwiftUI с целевой версией macOS 13 или новее. Замените сгенерированные объявления приложения и представления содержимого этой полной программой, оставив в цели только одно объявление приложения @main. В примере используются только локальные синтетические метки.
import SwiftUI
import Foundation
@main
struct WidthDemoApp: App {
var body: some Scene {
WindowGroup {
WidthDemoView()
}
}
}
struct WidthDemoView: View {
@State private var measuredWidth: CGFloat = 0
@State private var offeredWidth: Double = 260
@State private var showLongLabel = false
private var label: String {
showLongLabel
? "Sample collection of blue circles"
: "Blue circles"
}
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text(label)
.font(.system(size: 16))
.lineLimit(1)
.padding(.horizontal, 12)
.padding(.vertical, 8)
.background(Color.blue.opacity(0.12))
.onGeometryChange(for: CGFloat.self) { proxy in
proxy.size.width
} action: { newWidth in
measuredWidth = newWidth
}
.frame(
width: CGFloat(offeredWidth),
alignment: .leading
)
Text(String(
format: "Measured badge: %.1f pt",
Double(measuredWidth)
))
.monospacedDigit()
.frame(width: 340, alignment: .leading)
Slider(value: $offeredWidth, in: 120...340)
Button("Change sample label") {
showLongLabel.toggle()
}
}
.frame(width: 360, alignment: .leading)
.padding(20)
}
}
Наблюдатель считывает proxy.size.width; Apple определяет GeometryProxy.size как размер представления-контейнера. Здесь наблюдаемое представление включает текст и отступы, а следующая за ним рамка задаёт независимо управляемое предложение ширины. См. GeometryProxy.size.
Измерение обновляет отдельное поле показаний. Оно не задаёт ширину измеряемой плашки. Фиксированная область демонстрации и ширина поля показаний упрощают проверку результата, не позволяя диагностическому тексту менять размер окружающего стека.
Возврат только CGFloat также позволяет сосредоточить наблюдение на ширине: изменение только высоты не меняет преобразованное значение. Apple рекомендует преобразовывать геометрию в конкретные нужные данные, поскольку она может меняться часто. См. onGeometryChange(for:of:action:).
Эти примеры проверены по полученным объявлениям API. Здесь их не компилировали и не запускали, поскольку доступная среда — Linux без инструментария Swift; соберите проект и изучите интерфейс в Xcode на Mac.
Куда поместить модификатор измерения?
Читайте цепочку модификаторов как последовательность обёрнутых представлений. Измерение следует разместить сразу после последнего влияющего на размер модификатора, который нужно включить. В объяснении макета Apple описано, как рамки оборачивают представления, а отступы меняют пространство, доступное их содержимому. См. Размещение простого представления.
| Положение измерения в примере | Что оно измеряет |
|---|---|
| До обоих модификаторов отступов | Текущую ширину макета текстового представления |
| После обоих модификаторов отступов | Текущую ширину плашки с отступами |
| После модификатора явной ширины рамки | Ширину внешней рамки |
На окружающем VStack |
Ширину стека |
Для наглядного расчёта предположим, что при текущем предложении ширины текстовое представление сообщает 96 пунктов. Добавление по 12 пунктов с каждого края даёт ширину плашки с отступами 120 пунктов. Внешняя рамка шириной 260 пунктов — ещё одно отдельное измерение. Эти числа поясняют границы модификаторов; это не результаты измерения строки из примера.
Чтобы разобраться в неожиданном значении, временно добавьте рамку в ту же позицию цепочки модификаторов, что и наблюдатель. Сравните эту границу с нужной областью измерения. Reader, добавленный ко всей строке, может правильно показать её ширину, даже если видимая метка занимает лишь небольшую её часть.
Reader на заднем плане для целевой версии macOS 12
Для целевой версии macOS 12 используйте GeometryReader на заднем плане, чтобы передавать ширину через preference. Применяемый ниже вариант API фона с замыканием доступен начиная с macOS 12. Apple рекомендует использовать фон, когда определяющим для макета должно быть изменяемое представление. См. background(alignment:content:).
Это полная альтернативная программа. Замените ею предыдущий пример, а не добавляйте второе объявление приложения.
import SwiftUI
import Foundation
private struct BadgeWidthKey: PreferenceKey {
static var defaultValue: CGFloat { 0 }
static func reduce(
value: inout CGFloat,
nextValue: () -> CGFloat
) {
value = nextValue()
}
}
@main
struct LegacyWidthDemoApp: App {
var body: some Scene {
WindowGroup {
LegacyWidthDemoView()
}
}
}
struct LegacyWidthDemoView: View {
@State private var measuredWidth: CGFloat = 0
@State private var showLongLabel = false
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text(showLongLabel ? "Blue circles collection" : "Blue circles")
.font(.system(size: 16))
.lineLimit(1)
.padding(.horizontal, 12)
.padding(.vertical, 8)
.background(Color.blue.opacity(0.12))
.background {
GeometryReader { proxy in
Color.clear.preference(
key: BadgeWidthKey.self,
value: proxy.size.width
)
}
}
.onPreferenceChange(BadgeWidthKey.self) { newWidth in
measuredWidth = newWidth
}
Text(String(
format: "Measured badge: %.1f pt",
Double(measuredWidth)
))
.frame(width: 340, alignment: .leading)
Button("Change sample label") {
showLongLabel.toggle()
}
}
.frame(width: 360, alignment: .leading)
.padding(20)
}
}
PreferenceKey передаёт значение к родительским представлениям, а onPreferenceChange получает изменения этого значения. Reader выдаёт измерение, вместо того чтобы присваивать состояние внутри замыкания своего содержимого. См. документацию Apple: PreferenceKey и onPreferenceChange(_:perform:).
Этот редьюсер намеренно рассчитан на одну плашку, выдающую значение. При повторном использовании того же ключа для нескольких дочерних представлений их значения будут объединяться; в этой реализации сохраняется последнее сведённое значение. Для независимого отслеживания меток создайте коллекцию измерений с ключами, а не используйте один скаляр для нескольких представлений.
Как убедиться, что измерение остаётся независимым?
Используйте первую программу как небольшую диагностическую последовательность:
- Начните с
Blue circlesи щедро предложенной ширины. Сравните цветную плашку с показанием. - Перетащите ползунок. Внешняя рамка изменится независимо; пока места достаточно, плашка может оставаться компактной.
- Выберите
Change sample label, затем уменьшите предложенную ширину. Проверяйте текущую ограниченную плашку, не ожидая, что она будет иметь идеальную ширину всей строки. - Переместите наблюдатель ниже явной рамки. Теперь он должен показывать ширину этой рамки.
- Перед повторным использованием измерения плашки верните наблюдатель над рамкой.
Не используйте наблюдаемую ширину в формуле, которая определяет ширину того же представления. Например, если задать для него рамку шириной, равной наблюдаемой ширине плюс отступы, в макет может снова и снова возвращаться всё большее значение. Это напрямую следует из созданной зависимости: наблюдение меняет состояние, состояние меняет размер, а новый размер вызывает следующее наблюдение.
Если задача состоит в выборе между полной меткой и компактной альтернативой, ViewThatFits в macOS 13 и новее может выбрать первое дочернее представление, идеальный размер которого помещается по ограниченным осям. Так можно обойтись без сохранения ширины. См. ViewThatFits.
Если несколько дочерних представлений нужно измерить и разместить вместе, используйте пользовательский Layout в macOS 13 и новее. Его методы измерения и размещения позволяют контейнеру опросить дочерние представления перед тем, как расположить их, оставляя это решение внутри процесса макета. См. Layout.
Дадут ли тот же ответ идеальный размер или метрики текста AppKit?
Используйте .fixedSize(horizontal: true, vertical: false) только в том случае, если нужно сохранить идеальную ширину текста. Apple указывает, что в этом случае текст может выйти за границы родительского представления. Добавление этого модификатора ради измерения меняет поведение отображаемого представления при определении размера, поэтому исходный ограниченный макет не сохраняется. См. fixedSize(horizontal:vertical:).
Количество символов тоже не подходит для измерения ширины. String.count в Swift считает значения Character, представленные расширенными графемными кластерами; оно не возвращает типографические размеры. Умножение этого количества на предполагаемую ширину символа не измеряет представление SwiftUI. См. Строки и символы.
Для задачи рисования в AppKit NSAttributedString.size() возвращает размер, необходимый для отображения этой атрибутированной строки, который может включать дробные значения. Apple советует округлять результат вверх при использовании его для задания размера представления. Это отвечает на вопрос об отображении текста; результат не включает отступы представления SwiftUI или окружающую рамку. См. size().
Для существующего макета TextKit 1 NSLayoutManager.usedRect(for:) сообщает область, занятую глифами, уже размещёнными в текстовом контейнере. Сам по себе этот вызов не создаёт глифы и не выполняет макет; вызовите ensureLayout(for:), если сначала нужно завершить макет. Эти API измеряют настроенный текстовый контейнер, поэтому отделяйте их результаты от геометрии SwiftUI. См. usedRect(for:) и ensureLayout(for:).
Для метки, уже отображаемой в интерфейсе SwiftUI на Mac, наблюдайте за её геометрией на нужной границе, оставьте показание независимым и проверьте изменения как содержимого, так и доступного пространства. Так вы получите полезное измерение ширины и сохраните макет, который хотели изучить.
Источники
onGeometryChange(for:of:action:) — Apple
GeometryReader — Apple
Добавление фона к представлению — Apple
size — Apple
background(alignment:content:) — Apple
PreferenceKey — Apple
onPreferenceChange(_:perform:) — Apple
ViewThatFits — Apple
Layout — Apple
fixedSize(horizontal:vertical:) — Apple
Строки и символы — Swift.org
size() — Apple
usedRect(for:) — Apple
ensureLayout(for:) — Apple