Para ajustar los controles laterales a una vista previa de SwiftUI con relación de aspecto, calcula el tamaño ajustado de la vista previa a partir del espacio disponible en la ventana y, luego, asigna esa misma altura a los controles. Este ejemplo para macOS mantiene las proporciones de una vista previa sintética y permite desplazarse por los controles que no caben.
La tarea consiste en crear un editor de escritorio redimensionable con una vista previa a la izquierda y un panel de controles de ancho fijo a la derecha. Aquí, «vista previa» significa contenido dentro de la aplicación en ejecución. El objetivo es alinear los bordes superior e inferior de distribución de ambos paneles cuando cambia el tamaño de la ventana.
¿Qué altura deben seguir los controles?
Empieza por definir el rectángulo que quieres igualar. Una vista previa ajustada a la relación de aspecto puede ocupar menos espacio que el contenedor que la rodea. Si los controles siguen la altura del contenedor, pueden extenderse por debajo de la vista previa visible aunque ambas vistas estén distribuidas correctamente.
Apple define la relación de aspecto como el ancho dividido por la altura. Para el contenido redimensionable, aspectRatio(_:contentMode:) con .fit conserva esa relación y ajusta el contenido al espacio propuesto. El ejemplo de la elipse en la documentación muestra por qué la forma ajustada y un marco cuadrado que la contiene pueden tener dimensiones distintas. Documentación de Apple sobre la relación de aspecto.
Esta implementación hace explícito el rectángulo ajustado. Reserva un ancho fijo para los controles y un espacio entre los paneles; luego, ajusta la vista previa al ancho restante y a la altura disponible. Ambos paneles reciben la altura calculada de la vista previa.
Esta es una política de distribución para este ejemplo: la vista previa determina la altura del área visible de los controles. Una lista larga de controles se desplaza dentro de esa área en lugar de hacer que la vista previa sea más alta. Apple documenta ScrollView como una vista que muestra una parte desplazable del contenido subyacente dentro de una región con desplazamiento. Documentación de Apple sobre vistas de desplazamiento.
Calcula el rectángulo ajustado una sola vez
Sean W y H las dimensiones disponibles del contenido después del relleno exterior, C el ancho de los controles, G el espacio entre paneles y R la relación entre el ancho y la altura de la vista previa. El cálculo es:
remainingWidth = max(0, W - C - G)
previewWidth = min(remainingWidth, H * R)
previewHeight = previewWidth / R
controlsHeight = previewHeight
El primer límite impide que la vista previa ocupe el espacio horizontal de los controles. El segundo impide que una ventana ancha produzca una vista previa más alta que el área disponible. Calcular la altura a partir del ancho elegido conserva la relación en ambos casos.
Por ejemplo, supongamos que el área de contenido antes del relleno mide 960 por 600 puntos. Con un relleno de 16 puntos en cada borde, un panel de controles de 220 puntos, un espacio de 16 puntos y una vista previa de 16:9, el área disponible mide 928 por 568 puntos. El ancho restante para la vista previa es de 692 puntos, lo que da una altura de 389,25 puntos. Por tanto, el área visible de los controles mide 220 por 389,25 puntos.
Estos son valores ilustrativos, no resultados medidos de una aplicación. También muestran por qué igualar los controles a los 568 puntos de altura disponible produciría una alineación incorrecta.
Usa CGFloat para las dimensiones de distribución y una relación positiva y finita. En Swift, let ratio: CGFloat = 16.0 / 9.0 hace explícito el cálculo previsto en coma flotante. Si más adelante tu aplicación admite relaciones configurables, valídalas antes de dividir.
Crea el ejemplo completo para macOS
Crea en Xcode una aplicación SwiftUI para macOS y establece macOS 13 o una versión posterior como destino de implementación. Reemplaza las declaraciones generadas de la aplicación y de la vista de contenido con este único archivo, dejando exactamente una declaración @main en el destino. El ejemplo no requiere recursos ni paquetes externos.
El requisito de versión de implementación se debe a la API de ajuste de tamaño de ventana que se usa aquí. La escena usa .contentMinSize, que, según la documentación de Apple, deriva el tamaño mínimo de la ventana de su contenido y no impone un tamaño máximo. Documentación de Apple sobre el redimensionamiento de ventanas, Documentación de Apple sobre el tamaño mínimo del contenido.
import SwiftUI
@main
struct MatchedPreviewApp: App {
var body: some Scene {
WindowGroup {
PreviewEditor()
.frame(minWidth: 640, minHeight: 360)
}
.windowResizability(.contentMinSize)
}
}
struct PreviewEditor: View {
@State private var intensity: Double = 0.5
@State private var showsLabel = true
private let ratio: CGFloat = 16.0 / 9.0
private let controlsWidth: CGFloat = 220
private let gap: CGFloat = 16
var body: some View {
GeometryReader { geometry in
let availableHeight = max(0, geometry.size.height)
let remainingWidth = max(
0,
geometry.size.width - controlsWidth - gap
)
let previewWidth = min(
remainingWidth,
availableHeight * ratio
)
let previewHeight = previewWidth / ratio
HStack(alignment: .top, spacing: gap) {
RoundedRectangle(cornerRadius: 12)
.fill(Color.blue.opacity(0.2 + intensity * 0.7))
.aspectRatio(ratio, contentMode: .fit)
.overlay {
Text("Sample composition")
.font(.headline)
.opacity(showsLabel ? 1 : 0)
}
.frame(
width: previewWidth,
height: previewHeight
)
ScrollView(.vertical) {
VStack(alignment: .leading, spacing: 16) {
Text("Preview controls")
.font(.headline)
Toggle("Show label", isOn: $showsLabel)
VStack(alignment: .leading, spacing: 8) {
Text("Color intensity")
Slider(value: $intensity, in: 0...1)
}
ForEach(1...8, id: \.self) { index in
Text("Sample setting \(index)")
.font(.caption)
}
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .leading)
}
.frame(
width: controlsWidth,
height: previewHeight
)
.background(Color.gray.opacity(0.08))
}
.frame(
width: geometry.size.width,
height: geometry.size.height,
alignment: .center
)
}
.padding(16)
}
}
GeometryReader proporciona el tamaño de la región asignada al editor. Apple lo describe como un elemento que define el contenido a partir de su propio tamaño y sistema de coordenadas, con un tamaño preferido flexible. Al colocar el relleno fuera del lector, el cálculo usa el área de contenido interior. Documentación de Apple sobre GeometryReader.
Los dos marcos fijos establecen la altura compartida. Apple documenta frame(width:height:alignment:) como un elemento que crea un marco invisible con las dimensiones especificadas. El fondo de los controles va después de ese marco, por lo que cubre el área visible de los controles, incluido cualquier espacio debajo de una lista corta. Documentación de Apple sobre marcos fijos.
El marco final centra toda la fila dentro del lector. No amplía ninguno de los paneles. Por tanto, una ventana alta puede tener espacio sin usar por encima y por debajo de la fila mientras los controles siguen ajustándose a la vista previa.
Conserva el estado y el comportamiento del ciclo de vida al redimensionar
Mantén los valores editables por el usuario separados de la geometría calculada. En el ejemplo, intensity y showsLabel son estados; el ancho y la altura de la vista previa son valores locales derivados del tamaño disponible actual.
Apple documenta el estado como almacenamiento administrado por SwiftUI, cuyos cambios actualizan las partes dependientes de la jerarquía de vistas. Esto permite mantener los valores del control deslizante y el botón de alternancia en el editor que los administra. Documentación de Apple sobre el estado.
El cálculo de distribución no contiene asignaciones de estado, llamadas de apariencia, trabajo asíncrono ni ciclos de retroalimentación de medición. Al redimensionar, cambian las dimensiones que usa la misma estructura de vistas. La etiqueta sigue presente y cambia de opacidad cuando cambia el botón de alternancia.
Al adaptar esto a un editor existente, conserva la propiedad de sus modelos y los tipos de enlace. Aplica la política de tamaño alrededor de la vista previa y los controles; no traslades la creación del modelo al cálculo geométrico. La aritmética de este ejemplo solo requiere las dimensiones disponibles y la relación elegida.
Comprueba el redimensionamiento, el desbordamiento y el orden de los modificadores
Usa las siguientes dimensiones como comprobaciones del cálculo. Describen el área de contenido antes del relleno exterior del ejemplo, no el marco exterior de la ventana. Los valores esperados están redondeados a dos decimales.
| Área de contenido | Tamaño de la vista previa | Área visible de los controles |
|---|---|---|
| 960 × 600 puntos | 692 × 389.25 | 220 × 389.25 |
| 640 × 360 puntos | 372 × 209.25 | 220 × 209.25 |
| 1200 × 400 puntos | 654.22 × 368 | 220 × 368 |
| 700 × 900 puntos | 432 × 243 | 220 × 243 |
La aritmética se comprobó con un pequeño cálculo en Python, que incluía aserciones para verificar que la vista previa ajustada no superara la altura disponible y que ambos paneles, junto con el espacio entre ellos, cupieran horizontalmente. Swift y Xcode no estaban disponibles en el entorno de investigación, así que se inspeccionó el programa completo de SwiftUI, pero no se compiló ni ejecutó.
En Xcode, ejecuta la aplicación y redimensiona la ventana hasta darle una forma ancha, baja y alta. Confirma que los bordes de los paneles permanezcan alineados, que los controles se desplacen cuando su contenido supere el área visible y que el control deslizante y el botón de alternancia conserven sus valores al redimensionar.
Si la alineación sigue siendo distinta tras adaptar el ejemplo, revisa estas condiciones específicas:
- Relleno adicional del panel: El relleno aplicado fuera del marco fijo de un solo panel aumenta el espacio total que ocupa. Mantén el relleno de los controles dentro de la vista de desplazamiento, como se muestra.
- Un contenedor más grande para la vista previa: Si añades un marco más grande alrededor de la vista previa, decide si los controles deben ajustarse a ese contenedor o al contenido ajustado. Este cálculo iguala el rectángulo ajustado.
- Contenido personalizado que se desborda: Un marco establece las dimensiones de distribución, pero no oculta automáticamente el dibujo que se sale de ellas. Apple documenta
.clipped()para recortar con forma rectangular; úsalo cuando quieras ocultar el desbordamiento intencional de la vista previa. Mantén los controles desplazables para que todos los ajustes sigan estando al alcance. Documentación de Apple sobre recorte. - Espacio insuficiente en el contenedor: El ejemplo depende del tamaño mínimo de contenido. Si se integra en un contenedor más pequeño, el ancho fijo de los controles puede dejar un ancho insuficiente para la vista previa. Elige un panel más estrecho, una disposición vertical o un mínimo explícito para ese contenedor.
¿Cuándo conviene usar un diseño personalizado?
Este enfoque es adecuado para un editor que recibe una región de ventana acotada y usa una relación de aspecto conocida. Su política de tamaño es explícita y no depende de medir el contenido interno de la vista previa.
Si tu componente reutilizable debe determinar su tamaño preferido a partir de sus vistas secundarias, considera usar un Layout personalizado. El contrato documentado por Apple separa la comunicación del tamaño compuesto mediante sizeThatFits(proposal:subviews:cache:) de la colocación de las vistas secundarias mediante placeSubviews(in:proposal:subviews:cache:). Los proxies de las vistas secundarias proporcionan operaciones de medición y colocación. Documentación de Apple sobre diseños.
Para el editor de escritorio con dimensiones acotadas que se muestra aquí, basta con un rectángulo ajustado compartido: reserva espacio horizontal para los controles, ajusta la vista previa a ambas dimensiones disponibles y asigna su altura al área visible de los controles. Así obtienes una regla de redimensionamiento clara y el contenido de los controles puede desplazarse.
Fuentes
- aspectRatio(_:contentMode:) — Documentación para desarrolladores de Apple.
- ScrollView — Documentación para desarrolladores de Apple.
- windowResizability(_:) — Documentación para desarrolladores de Apple.
- contentMinSize — Documentación para desarrolladores de Apple.
- GeometryReader — Documentación para desarrolladores de Apple.
- frame(width:height:alignment:) — Documentación para desarrolladores de Apple.
- State — Documentación para desarrolladores de Apple.
- clipped(antialiased:) — Documentación para desarrolladores de Apple.
- Layout — Documentación para desarrolladores de Apple.