Para que una vista de desplazamiento de SwiftUI se ajuste al contenido breve, mide su contenido interno y establece la altura de la ventana gráfica en el menor valor entre esa medida y un límite elegido. El contenido más largo se desplaza dentro de ese límite. Este recorrido implementa ese comportamiento en un pequeño panel de etiquetas de tu propia app para Mac.
¿Por qué una vista de desplazamiento corta deja espacio vacío?
Empieza por distinguir dos dimensiones: la altura del contenido y la altura de la ventana gráfica que lo muestra. Apple describe ScrollView como un componente que muestra una parte de su contenido subyacente y ajusta la parte visible durante el desplazamiento. Un grupo pequeño de etiquetas no determina por sí solo la altura que prefieres para la ventana gráfica. La documentación de ScrollView de Apple explica esta distinción.
Para un panel compacto de escritorio, define una regla de tamaño explícita: viewportHeight = min(contentHeight, maximumHeight). Mide el contenido al ancho que realmente recibe, incluido cualquier relleno que deba desplazarse con él. Aplica el resultado a la vista de desplazamiento externa. min(_:_:) de Swift devuelve el menor valor, mientras que frame(width:height:alignment:) de SwiftUI establece una altura fija cuando se proporciona el argumento de altura. Swift min(_:_:), Marco fijo de SwiftUI.
A modo de ejemplo, supongamos que el máximo es de 240 puntos. Un contenido que mida 86 puntos produce una ventana gráfica de 86 puntos; uno que mida 410 puntos produce una ventana gráfica de 240 puntos, y el contenido restante queda accesible mediante el desplazamiento. Son valores ilustrativos, no mediciones del ejemplo de abajo.
| Cambio | Qué controla | ¿Establece la altura compacta deseada? |
|---|---|---|
| Altura medida del contenido, limitada y aplicada a la vista de desplazamiento | Altura de la ventana gráfica | Sí, en el diseño acotado que se muestra abajo |
.scrollBounceBehavior(.basedOnSize) |
Comportamiento de rebote en relación con el tamaño del contenido | No |
.fixedSize(horizontal: false, vertical: true) en las etiquetas |
Tamaño vertical ideal del contenido | Ayuda con la medición; la ventana gráfica externa sigue necesitando su regla de altura |
Apple documenta .basedOnSize como una configuración de rebote, y fixedSize(horizontal:vertical:) como una forma de fijar una vista en su tamaño ideal en las dimensiones seleccionadas. Ninguna de las dos descripciones establece una altura de ventana gráfica derivada del contenido. Comportamiento de rebote del desplazamiento, Tamaño fijo por eje.
Crear un panel compacto con una altura máxima de desplazamiento
Usa una instalación actual de Xcode cuyo SDK de macOS incluya la sobrecarga de un solo valor de onGeometryChange(for:of:action:). Actualmente, Apple documenta que esa sobrecarga está disponible desde macOS 13.0. El ejemplo completo de abajo requiere macOS 13.3 o posterior porque también usa scrollBounceBehavior(_:axes:), disponible desde macOS 13.3. Callback de cambio de geometría, Disponibilidad del modificador de rebote.
Crea un proyecto de app para macOS con SwiftUI. Sustituye la declaración de app y la vista de contenido generadas por este programa de un solo archivo, dejando una sola declaración @main en el destino. Todas las etiquetas son cadenas locales ficticias. El control Show more labels alterna entre contenido breve y extenso.
import SwiftUI
@main
struct CompactLabelsApp: App {
var body: some Scene {
WindowGroup {
CompactLabelsView()
}
}
}
struct CompactLabelsView: View {
@State private var showMore = false
@State private var viewportHeight: CGFloat = 240
private var labels: [String] {
if showMore {
return (1...18).map { number in
"Sample label \(number): a longer description that can wrap when the panel becomes narrower."
}
}
return ["Sample label A", "Sample label B"]
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Toggle("Show more labels", isOn: $showMore)
ScrollView(.vertical) {
VStack(alignment: .leading, spacing: 8) {
if labels.isEmpty {
Text("No labels to display")
} else {
ForEach(labels.indices, id: \.self) { index in
Text(labels[index])
}
}
}
.font(.body)
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(12)
.onGeometryChange(for: CGFloat.self) { proxy in
min(proxy.size.height.rounded(.up), 240)
} action: { newHeight in
viewportHeight = newHeight
}
}
.frame(height: viewportHeight)
.scrollBounceBehavior(.basedOnSize, axes: .vertical)
.border(Color.secondary.opacity(0.4))
Text("The label panel grows up to 240 points.")
.font(.caption)
}
.padding(20)
.frame(
maxWidth: .infinity,
maxHeight: .infinity,
alignment: .topLeading
)
.frame(minWidth: 300, minHeight: 360)
}
}
Se inspeccionó el programa, pero aquí no se compiló ni ejecutó porque este entorno no tiene un compilador de Swift ni un entorno de ejecución de macOS. Compílalo en Xcode y realiza las comprobaciones de abajo antes de incorporarlo a una interfaz existente.
Por qué la medición debe hacerse dentro de la vista de desplazamiento
El callback de geometría observa la pila interna con relleno. Medir la vista de desplazamiento externa devolvería la ventana gráfica que ya asignaste, creando una regla de tamaño circular en lugar de descubrir la altura que necesita el contenido.
La transformación devuelve únicamente la altura redondeada y limitada. Apple recomienda transformar la geometría en el valor que necesitas y evitar actualizaciones generales ante cambios frecuentes de geometría. En este caso, los cambios en la posición de desplazamiento no afectan al cálculo, y el contenido que supere el límite seguirá produciendo el mismo resultado de 240 puntos. Guía de Apple sobre cambios de geometría.
Redondear hacia arriba evita asignar deliberadamente una ventana gráfica ligeramente más corta que el contenido medido. El estado inicial de 240 puntos proporciona una ventana gráfica utilizable antes de que llegue la medición; es un valor de inicialización, no la altura medida del contenido. El callback lo reemplaza después.
Por qué importan el ancho y el orden de los modificadores
Mantén flexible el tamaño horizontal. El ejemplo usa .fixedSize(horizontal: false, vertical: true) para que las etiquetas puedan ocupar el ancho disponible y conservar su altura vertical ideal. Apple advierte que fijar una dimensión ideal puede hacer que una vista supere los límites de su contenedor; por eso, fijar ambas dimensiones no es una opción predeterminada adecuada para etiquetas con ajuste de línea en un panel estrecho. Documentación de fixedSize(horizontal:vertical:) de Apple.
El relleno de 12 puntos aparece antes del modificador de medición, por lo que forma parte de la altura medida del contenido. El borde aparece después de asignar la altura externa y marca el límite de la ventana gráfica. Esta disposición facilita ver si el espacio sin usar pertenece al panel desplazable o a la ventana que lo contiene.
Comprobar el contenido breve, el ajuste de línea y la reducción
Sigue estos pasos para comprobar el comportamiento del ejemplo y repetir la verificación después de colocar el panel en el contenedor real de tu app:
- Inicia la app con
Show more labelsdesactivado. El borde debe rodear las dos etiquetas y su relleno, con el texto inferior justo debajo. - Activa
Show more labels. El panel debe crecer hasta el límite de 240 puntos. Desplázate para confirmar que se puede llegar a la última etiqueta. - Reduce y aumenta el ancho de la ventana. Las etiquetas con ajuste de línea deben cambiar de altura sin perder sus líneas finales.
- Vuelve a desactivar el control. El panel debe reducirse a la altura del contenido breve.
- Cambia temporalmente el arreglo corto por
[]. El mensaje explícitoNo labels to displaydebe determinar la altura del contenido. - Repite la prueba con los estilos de texto y los controles que rodean el panel real.
El estado vacío es una decisión deliberada de interfaz: este ejemplo conserva un mensaje y el relleno cuando el arreglo está vacío. Si lo que quieres es eliminar el panel por completo, omite condicionalmente toda la vista de desplazamiento cuando no haya etiquetas. Esa es una decisión de producto independiente de medir un panel no vacío.
La ventana del ejemplo conserva su altura mínima. El código ajusta la ventana gráfica de las etiquetas; no solicita que la ventana del Mac se reduzca cada vez que se eliminan etiquetas. Cualquier espacio debajo del texto inferior pertenece al diseño externo, fuera de la vista de desplazamiento con borde.
Diagnosticar los espacios restantes o las etiquetas recortadas
Si el panel sigue siendo alto, comprueba primero dónde está conectado el modificador de medición. Debe observar la pila de contenido después de su relleno interno. Luego revisa si los contenedores antecesores tienen una altura o una altura mínima explícita que reserve espacio alrededor del panel. Apple documenta que una altura proporcionada al marco es una dimensión fija, por lo que un marco externo independiente puede conservar espacio aunque la ventana gráfica interna se reduzca. Tamaño del marco en SwiftUI.
Si el contenido sigue teniendo una altura inesperada, elimina la expansión vertical de la subvista medida. Para este patrón de etiquetas pequeñas, evita un espaciador vertical o un marco de contenido cuya altura mínima dependa de la ventana gráfica. Esas opciones hacen que la altura del contenido dependa de la ventana gráfica y socavan la medición independiente que necesita la regla de tamaño.
Si se recorta el relleno inferior, confirma que esté incluido antes de medir. Si el texto se ajusta de otra forma después de integrarlo, revisa el ancho real que ofrece el nuevo contenedor. Una altura medida para un ancho no debe tratarse como una altura permanente para otro ancho.
Ajusta el límite de 240 puntos al espacio que lo contiene. El ejemplo proporciona a la ventana una altura mínima suficiente para sus controles, el relleno, el texto inferior y el panel con altura limitada. Un contenedor más bajo necesita un presupuesto menor o un diseño externo distinto; este límite fijo no calcula automáticamente el espacio que queda debajo de otros controles.
Usa este enfoque para un conjunto pequeño de etiquetas cuyo diseño se realiza de inmediato. Apple describe LazyVStack como un componente que crea elementos solo cuando se necesitan para mostrarlos en pantalla. Por tanto, no des por sentado que reemplazar la pila interna por una pila diferida conserva la misma estrategia de medición del contenido completo. Para una colección grande, suele ser más adecuado usar una ventana gráfica estable con contenido diferido. Documentación de LazyVStack de Apple.
¿Cuándo corresponde medir con AppKit y TextKit?
Para este panel de etiquetas de SwiftUI, mide la subvista de SwiftUI renderizada. Medir el texto con AppKit introduciría otra configuración de diseño que tendría que coincidir con las etiquetas mostradas.
Si tu app ya gestiona un NSScrollView, conserva la misma distinción entre documento y ventana gráfica. Apple define documentView como la vista por la que se desplaza, mientras que contentSize es el tamaño de la vista de contenido de la vista de desplazamiento. Pese a su nombre, contentSize no mide el documento completo. documentView de AppKit, contentSize de AppKit.
Para un diseño TextKit existente basado en NSLayoutManager, usedRect(for:) devuelve el rectángulo delimitador de los glifos que ya están distribuidos en un contenedor de texto; Apple especifica que consultarlo no genera glifos ni realiza el diseño. Cuando se necesita distribuir el contenedor completo, ensureLayout(for:) es el método documentado para forzar primero ese diseño. usedRect(for:) de TextKit, ensureLayout(for:) de TextKit.
Aun así, el rectángulo de glifos no corresponde al marco completo de la vista de texto. Apple documenta espacio adicional alrededor del contenedor de texto mediante textContainerInset, además del relleno entre fragmentos de línea que hay dentro. Ten en cuenta la configuración real de la vista de texto en lugar de copiar directamente la altura de los glifos en una ventana gráfica desplazable. Estos métodos describen la ruta de NSLayoutManager; no son una receta de tamaño universal para todos los componentes de texto. textContainerInset de AppKit.
Para un panel pequeño de etiquetas de SwiftUI, la solución práctica es mantener flexible el ancho, medir el contenido interno con su relleno y limitar la altura de la ventana gráfica externa. Comprueba el resultado con contenido breve y extenso; después, pruébalo dentro del diseño contenedor real para que las restricciones que lo rodean permitan el mismo comportamiento compacto.
Fuentes
- ScrollView — Apple
- min(::) — Apple
- frame(width:height:alignment:) — Apple
- scrollBounceBehavior(_:axes:) — Apple
- fixedSize(horizontal:vertical:) — Apple
- onGeometryChange(for:of:action:) — Apple
- LazyVStack — Apple
- documentView — Apple
- contentSize — Apple
- usedRect(for:) — Apple
- ensureLayout(for:) — Apple
- textContainerInset — Apple