Para medir una etiqueta en tu app para Mac sin estirarla, añade onGeometryChange a la vista que quieras inspeccionar. Si tu versión de implementación es anterior, coloca un GeometryReader en su fondo. La posición del modificador determina qué ancho lees.
Este tutorial mide una etiqueta sintética, incluido su relleno, y mantiene el diseño que la rodea independiente del resultado. También explica cómo distinguir el ancho actual de la etiqueta de su ancho ideal y del ancho de su contenedor.
¿Por qué un lector de geometría hace que el diseño se expanda?
El diseño de SwiftUI implica propuestas y respuestas: un padre propone espacio y sus hijos informan de los tamaños calculados. Añadir un frame introduce otra vista en esa jerarquía, por lo que la etiqueta y su frame pueden tener anchos diferentes. Apple explica este proceso en Diseñar una vista sencilla.
Un GeometryReader es un contenedor con un tamaño preferido flexible. Por eso, envolver una etiqueta compacta en uno cambia el elemento de diseño que se presenta al padre. Estás midiendo el contenedor del lector, que puede ocupar mucho más espacio del que necesita el texto. Consulta la documentación de GeometryReader de Apple.
Para observar, añade la medición a la vista existente. Cuando sea necesario usar un lector en el fondo, su ubicación importa: Apple documenta que un fondo recibe el tamaño de la vista a la que está asociado. Esto permite que la etiqueta determine el área de medición. Consulta Añadir un fondo a tu vista.
Antes de elegir una implementación, decide qué pregunta necesita responder tu interfaz:
| Tu pregunta | Enfoque adecuado |
|---|---|
| ¿Qué ancho tiene esta etiqueta en su diseño actual? | Observa la geometría de la etiqueta. |
| ¿Qué ancho tiene la insignia con relleno que rodea la etiqueta? | Observa después del relleno. |
| ¿Qué ancho tiene la fila que la contiene? | Observa la fila o su frame. |
| ¿Qué ancho preferiría tener todo el texto? | Investiga el tamaño ideal por separado. |
| ¿Cuál de varias presentaciones cabe? | Considera ViewThatFits. |
| ¿Cómo se deben medir y posicionar varios elementos secundarios en conjunto? | Considera un Layout personalizado. |
Esta distinción evita un error común: tratar el ancho de una etiqueta restringida y posiblemente truncada como si fuera el espacio necesario para mostrar toda la cadena.
Medir el ancho actual en macOS 13 y versiones posteriores
La documentación actual de Apple indica que la sobrecarga de acción de un solo valor de onGeometryChange(for:of:action:) está disponible en macOS 13 y versiones posteriores. Esta transforma un proxy de geometría en un valor equatable e invoca una acción cuando dicho valor cambia. Usa un SDK que incluya esta API y conserva la acción de un parámetro que se muestra aquí. Consulta onGeometryChange(for:of:action:).
Crea en Xcode un proyecto de app para macOS con SwiftUI y un destino de implementación de macOS 13 o posterior. Sustituye las declaraciones generadas de la app y la vista de contenido por este programa completo y deja una sola declaración de app @main en el destino. El ejemplo solo usa etiquetas sintéticas locales.
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)
}
}
El observador lee proxy.size.width; Apple define GeometryProxy.size como el tamaño de la vista contenedora. Aquí, la vista observada incluye el texto y el relleno, mientras que el frame posterior proporciona una propuesta de ancho controlada de forma independiente. Consulta GeometryProxy.size.
La medición actualiza una lectura aparte. No establece el ancho de la insignia medida. El área de demostración fija y el ancho de la lectura facilitan inspeccionar el resultado sin permitir que el texto de diagnóstico cambie el tamaño de la pila que lo rodea.
Devolver solo CGFloat también mantiene la observación centrada en el ancho: un cambio exclusivo de altura no cambia el valor transformado. Apple recomienda transformar la geometría en los datos específicos que necesitas, porque puede cambiar con frecuencia. Consulta onGeometryChange(for:of:action:).
Estos ejemplos se revisaron contrastándolos con las declaraciones de API obtenidas. No se compilaron ni ejecutaron aquí porque el entorno disponible es Linux y no tiene una cadena de herramientas de Swift; compila e inspecciona la interfaz en Xcode en un Mac.
¿Dónde debe ir el modificador de medición?
Lee la cadena de modificadores como una sucesión de vistas envueltas. La medición debe ir inmediatamente después del último modificador que afecta al tamaño y que quieras incluir. La explicación de diseño de Apple describe cómo los frames envuelven las vistas y cómo el relleno cambia el espacio disponible para su contenido. Consulta Diseñar una vista sencilla.
| Ubicación de la medición en el ejemplo | Qué observa |
|---|---|
| Antes de ambos modificadores de relleno | El ancho de diseño actual de la vista de texto |
| Después de ambos modificadores de relleno | El ancho actual de la insignia con relleno |
| Después del frame de ancho explícito | El ancho del frame exterior |
En el VStack que la rodea |
El ancho de la pila |
Como cálculo ilustrativo, supongamos que una vista de texto informa de 96 puntos con su propuesta actual. Añadir 12 puntos en cada borde horizontal produce un ancho con relleno de 120 puntos. Un frame exterior de 260 puntos es otra medición distinta. Estas cifras ilustran los límites entre modificadores; no son resultados medidos para la cadena del ejemplo.
Para investigar un valor inesperado, coloca temporalmente un borde en la misma posición de la cadena de modificadores que el observador. Compara ese límite con el objetivo previsto. Un lector asociado a una fila completa puede informar correctamente el ancho de la fila aunque la etiqueta visible solo ocupe una pequeña parte.
Usar un lector en el fondo con un destino de macOS 12
Para un destino de implementación de macOS 12, usa un GeometryReader en el fondo para publicar el ancho mediante una preferencia. La API de fondo con cierre final que se muestra a continuación está disponible a partir de macOS 12. Apple recomienda usar fondos cuando se quiere que la vista modificada determine el diseño. Consulta background(alignment:content:).
Este es un programa alternativo completo. Sustituye el ejemplo anterior por este en lugar de añadir una segunda declaración de app.
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 lleva un valor hacia los ancestros, y onPreferenceChange recibe los cambios de ese valor. El lector emite la medición en lugar de asignar estado dentro de su cierre de contenido. Consulta la documentación de Apple sobre PreferenceKey y onPreferenceChange(_:perform:).
Este reductor supone deliberadamente que solo emite una insignia. Si se reutilizara la misma clave para varios elementos secundarios, sus valores se combinarían y esta implementación conservaría el último valor reducido. Para hacer un seguimiento independiente de varias etiquetas, diseña una colección de mediciones con claves en lugar de usar un único escalar para representar varias vistas.
¿Cómo puedes comprobar que la medición se mantiene independiente?
Usa el primer programa como una pequeña secuencia de diagnóstico:
- Empieza con
Blue circlesy un ancho ofrecido generoso. Compara la insignia de color con la lectura. - Arrastra el control deslizante. El frame exterior cambia de forma independiente; la insignia puede seguir siendo compacta mientras haya espacio suficiente.
- Selecciona
Change sample labely reduce el ancho ofrecido. Inspecciona la insignia restringida actual en lugar de esperar el ancho ideal de la cadena completa. - Mueve el observador debajo del frame explícito. Ahora debería indicar el ancho de ese frame.
- Vuelve a colocar el observador por encima del frame antes de usar la medición de la insignia en otro lugar.
Mantén el ancho observado fuera de la fórmula que determina el ancho de esa misma vista. Por ejemplo, establecer su frame en el ancho observado más el relleno puede devolver repetidamente un resultado mayor al diseño. Esto se deriva directamente de la dependencia que creas: la observación cambia el estado, el estado cambia el tamaño y el nuevo tamaño produce otra observación.
Si tu tarea real consiste en elegir entre una etiqueta completa y una alternativa compacta, ViewThatFits en macOS 13 y versiones posteriores puede seleccionar el primer elemento secundario cuyo tamaño ideal quepa en los ejes restringidos. Así quizá no necesites almacenar ningún ancho. Consulta ViewThatFits.
Si es necesario medir y posicionar varios elementos secundarios en conjunto, usa un Layout personalizado en macOS 13 y versiones posteriores. Sus métodos de medición y posicionamiento permiten que el contenedor consulte las subvistas antes de colocarlas y mantiene esa decisión dentro del proceso de diseño. Consulta Layout.
¿Pueden el tamaño ideal o las métricas de texto de AppKit responder a la misma pregunta?
Usa .fixedSize(horizontal: true, vertical: false) solo si quieres conservar el ancho ideal del texto. Apple documenta que esto puede hacer que el texto supere los límites de su padre. Añadirlo para obtener una medición cambia el comportamiento de tamaño de la vista mostrada, por lo que no conserva el diseño original restringido. Consulta fixedSize(horizontal:vertical:).
El recuento de caracteres tampoco sirve para medir el ancho. String.count de Swift cuenta valores Character representados por clústeres de grafemas extendidos; no devuelve dimensiones tipográficas. Multiplicar ese recuento por un ancho de carácter aproximado no mide la vista de SwiftUI. Consulta Cadenas y caracteres.
Para una tarea de dibujo con AppKit, NSAttributedString.size() devuelve el tamaño necesario para dibujar esa cadena con atributos, que puede incluir dimensiones fraccionarias. Apple recomienda redondear hacia arriba al usar ese resultado para dimensionar una vista. Esto responde a una pregunta sobre dibujo de texto; no incluye el relleno de una vista de SwiftUI ni el frame que la contiene. Consulta size().
Para un diseño existente de TextKit 1, NSLayoutManager.usedRect(for:) informa del área que ocupan los glifos dispuestos actualmente en su contenedor de texto. Por sí misma, no genera glifos ni realiza el diseño; llama a ensureLayout(for:) cuando primero hay que completar el diseño. Estas API miden ese contenedor de texto configurado, así que mantén sus resultados separados de la geometría de SwiftUI. Consulta usedRect(for:) y ensureLayout(for:).
Para la etiqueta que ya se muestra en la interfaz de SwiftUI de tu Mac, observa su geometría en el límite que necesites, mantén la lectura independiente y prueba cambios tanto en el contenido como en el espacio disponible. Así obtendrás un ancho útil y conservarás el diseño que querías inspeccionar.
Fuentes
onGeometryChange(for:of:action:) — Apple
Diseñar una vista sencilla — Apple
GeometryReader — Apple
Añadir un fondo a tu vista — Apple
size — Apple
background(alignment:content:) — Apple
PreferenceKey — Apple
onPreferenceChange(_:perform:) — Apple
ViewThatFits — Apple
Layout — Apple
fixedSize(horizontal:vertical:) — Apple
Cadenas y caracteres — Swift.org
size() — Apple
usedRect(for:) — Apple
ensureLayout(for:) — Apple