Adaptive Layout es un enfoque de diseño de interfaces donde la aplicación utiliza diferentes diseños para diferentes tipos de dispositivos y orientaciones de pantalla. A diferencia del responsive design, el adaptive layout cambia entre diseños predefinidos en puntos de quiebre. Este artículo analiza Size Classes y UITraitCollection en iOS, los calificadores sw600dp/layout-w600dp en Android y proporciona ejemplos prácticos.
Puntos clave
Adaptive Layout es una estrategia de UI donde el desarrollador crea diseños separados para cada tipo de dispositivo y cambia entre ellos según las características de la pantalla. A diferencia del responsive design donde los elementos fluyen suavemente, el adaptive layout usa cambios discretos: teléfono — un diseño, tablet — otro, escritorio — un tercero.
Apple introdujo Size Classes en iOS 8 (2014) junto con las aplicaciones universales (binarios que funcionan tanto en iPhone como en iPad). Google presentó el calificador smallestWidth (sw
Las guías modernas de Apple HIG y Google Material Design coinciden en que el adaptive layout es esencial para aplicaciones que admiten más de un tamaño de pantalla. Google Play marca la falta de adaptación para tablets como una desventaja. App Store no tiene un requisito formal, pero los usuarios de iPad esperan una experiencia nativa, no una aplicación de iPhone estirada.
Size Classes son una característica de iOS que define el espacio disponible como Compact (Compacto) o Regular (Regular) en cada eje. UITraitCollection es un sistema de iOS para pasar características del entorno a través de la jerarquía de vistas: tamaño, escala de pantalla, capacidad force touch, gama de colores (claro/oscuro), ajustes de accesibilidad.
| Dispositivo / Orientación | Horizontal | Vertical |
|---|---|---|
| iPhone retrato (todos los modelos) | Compact | Regular |
| iPhone Plus/Pro Max paisaje | Regular | Compact |
| iPhone SE / mini paisaje | Compact | Compact |
| iPad retrato | Regular | Regular |
| iPad paisaje | Regular | Regular |
| iPad Split View (1/3 pantalla) | Compact | Regular |
UITraitCollection es formado por el sistema y se pasa desde UIApplication a través de UIWindow hasta cada UIView. Cuando cambia la orientación o el tamaño de la ventana (Split View), el sistema genera un nuevo UITraitCollection y llama a traitCollectionDidChange. En SwiftUI, los cambios se rastrean a través de Environment Values y redibujan la vista automáticamente.
La adaptación mediante UITraitCollection en UIKit se implementa suscribiéndose a los cambios de trait collection y reconstruyendo el layout. El siguiente ejemplo muestra un controlador que cambia entre una pila vertical para iPhone y un diseño dividido para iPad.
// Controlador adaptativo con Size Classes
import UIKit
final class AdaptiveViewController: UIViewController {
private let compactStack = UIStackView()
private let regularStack = UIStackView()
private let sidebar = UIView()
private let content = UIView()
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
updateLayout(for: traitCollection)
}
override func traitCollectionDidChange(
_ previousTraitCollection: UITraitCollection?
) {
super.traitCollectionDidChange(previousTraitCollection)
if traitCollection.horizontalSizeClass
!= previousTraitCollection?.horizontalSizeClass {
updateLayout(for: traitCollection)
}
}
private func updateLayout(for traits: UITraitCollection) {
if traits.horizontalSizeClass == .regular {
showRegularLayout() // iPad: barra lateral + contenido
} else {
showCompactLayout() // iPhone: barra de pestañas + push
}
}
private func showRegularLayout() {
view.subviews.forEach { $0.removeFromSuperview() }
regularStack.addArrangedSubview(sidebar)
regularStack.addArrangedSubview(content)
regularStack.frame = view.bounds
regularStack.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(regularStack)
}
private func showCompactLayout() {
view.subviews.forEach { $0.removeFromSuperview() }
compactStack.addArrangedSubview(content)
compactStack.frame = view.bounds
compactStack.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(compactStack)
}
private func setupViews() {
sidebar.backgroundColor = .systemGray6
content.backgroundColor = .systemBackground
compactStack.axis = .vertical
regularStack.axis = .horizontal
}
}El método traitCollectionDidChange se llama ante cualquier cambio de traits, pero solo comprobamos horizontalSizeClass — el indicador más significativo para cambiar el diseño. En iOS 17+ se recomienda usar UIViewController.horizontalSizeClass como propiedad computada para actualizaciones reactivas sin suscripción a cambios.
Android proporciona dos calificadores principales de ancho: sw
| Calificador | Condición | Ejemplo de dispositivo |
|---|---|---|
| layout-sw600dp | Ancho mínimo ≥ 600 dp | iPad (768 dp), Pixel C (900 dp) |
| layout-w600dp | Ancho actual ≥ 600 dp | iPad en Split View (600 dp), teléfono en paisaje |
| layout-land | Orientación landscape | Cualquier dispositivo girado horizontalmente |
| layout-sw720dp | Ancho mínimo ≥ 720 dp | iPad Pro (833 dp), Galaxy Tab S9 (800 dp) |
Jerarquía de recursos en Android: res/layout/activity_main.xml (teléfono), res/layout-sw600dp/activity_main.xml (tablet 7"), res/layout-w600dp-land/activity_main.xml (teléfono en paisaje). Android selecciona el calificador más específico que coincida con la configuración actual del dispositivo. Cuando no hay un archivo coincidente, se usa el recurso base de res/layout/.
El desarrollo moderno de Android usa Jetpack WindowManager para determinar el tamaño de la ventana en lugar de leer directamente los recursos. Esto es especialmente importante para dispositivos plegables donde el tamaño de pantalla cambia dinámicamente (Galaxy Fold se despliega de 6.2" a 7.6").
// Adaptación mediante Jetpack WindowManager
import androidx.window.core.layout.WindowHeightSizeClass
import androidx.window.core.layout.WindowSizeClass
import androidx.window.core.layout.WindowWidthSizeClass
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun AdaptiveScreen() {
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
FlowRow(
verticalAlignment = Arrangement.Top,
horizontalArrangement = when (windowSizeClass.windowWidthSizeClass) {
WindowWidthSizeClass.EXPANDED -> Arrangement.SpaceEvenly
else -> Arrangement.Start
}
) {
when (windowSizeClass.windowWidthSizeClass) {
WindowWidthSizeClass.COMPACT -> {
CompactContent()
}
WindowWidthSizeClass.MEDIUM -> {
MediumContent()
}
WindowWidthSizeClass.EXPANDED -> {
ExpandedContent()
}
}
}
}
@Composable
private fun CompactContent() {
Column(modifier = Modifier.padding(16.dp)) {
Text("Teléfono", style = MaterialTheme.typography.headlineSmall)
ListContent()
}
}
@Composable
private fun MediumContent() {
Row(modifier = Modifier.padding(24.dp)) {
NavigationRail { ... }
Column { ListContent() }
}
}
@Composable
private fun ExpandedContent() {
Row(modifier = Modifier.padding(32.dp)) {
PermanentNavigationDrawer { ... }
Column {
ListContent()
}
DetailPane()
}
}El componente currentWindowAdaptiveInfo() de la biblioteca androidx.window adapta la interfaz a cualquier dispositivo: teléfono, tablet, plegable, ChromeOS. Clases de ancho: COMPACT (0–600 dp), MEDIUM (600–840 dp), EXPANDED (>840 dp). Clases de alto: COMPACT (0–480 dp), MEDIUM (480–900 dp), EXPANDED (>900 dp).
SwiftUI proporciona herramientas integradas para el layout adaptativo sin usar directamente Size Classes. AnyLayout, ViewThatFits y GeometryReader permiten construir interfaces que se ajustan automáticamente al espacio disponible.
// Adaptación SwiftUI mediante AnyLayout y ViewThatFits
import SwiftUI
struct AdaptiveGrid: View {
@Environment(\.horizontalSizeClass) private var hSizeClass
var body: some View {
let layout = hSizeClass == .regular
? AnyLayout(HStackLayout())
: AnyLayout(VStackLayout())
layout {
Label("Favoritos", systemImage: "star")
Label("Recientes", systemImage: "clock")
Label("Ajustes", systemImage: "gear")
}
.padding()
}
}
// ViewThatFits — selección automática de layout
struct SmartLayout: View {
var body: some View {
ViewThatFits {
HStack { // Prioridad 1: horizontal
CardView()
CardView()
}
VStack { // Si no cabe — vertical
CardView()
CardView()
}
}
}
}ViewThatFits es una potente herramienta de SwiftUI que selecciona automáticamente la primera vista hija que cabe en el espacio disponible sin recortar. Esto elimina la necesidad de verificar explícitamente Size Classes para cambios adaptativos simples. AnyLayout permite cambiar el tipo de layout (HStack/VStack) sin if-else en el cuerpo de la vista.
Jetpack Compose usa WindowSizeClass (de la biblioteca Material 3 adaptive) y BoxWithConstraints para el diseño adaptativo. A diferencia de los calificadores XML, Compose determina el tamaño de la ventana en tiempo de ejecución y recompone la IU reactivamente ante cambios de configuración.
// Jetpack Compose adaptativo mediante BoxWithConstraints
@Composable
fun AdaptiveList(items: List<String>) {
BoxWithConstraints {
val width = maxWidth
if (width >= 600.dp) {
// Tablet: dos columnas (list-detail)
Row(modifier = Modifier.fillMaxSize()) {
LazyColumn(modifier = Modifier.weight(1f)) {
items(items) { item ->
ListItem(text = item)
}
}
var selected by remember { mutableStateOf(items.first()) }
DetailPanel(item = selected)
.weight(2f)
}
} else {
// Teléfono: una columna con navegación
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(items) { item ->
ListItem(text = item, onClick = { navigateToDetail(item) })
}
}
}
}
}
@Composable
fun AdaptiveListMaterial3() {
val windowClass = currentWindowAdaptiveInfo().windowSizeClass
AdaptiveLayout(
layout = windowClass.windowWidthSizeClass
) {
when (windowClass.windowWidthSizeClass) {
WindowWidthSizeClass.COMPACT -> {
ListDetailScaffold(
isDetailOnly = false,
list = { ListPane() },
detail = { DetailPane() }
)
}
else -> {
ListDetailScaffold(
isDetailOnly = false,
list = { ListPane() },
detail = { DetailPane() }
)
}
}
}
}Material 3 Adaptive Layouts proporciona componentes listos: ListDetailScaffold, SupportingPaneScaffold, NavigationSuiteScaffold. Estos componentes se adaptan automáticamente a WindowWidthSizeClass — cambian entre apilado (COMPACT) y lado a lado (MEDIUM/EXPANDED). El desarrollador solo necesita elegir un patrón y pasar los paneles de contenido.
Preguntas frecuentes
sw600dp (smallestWidth) — ancho mínimo de pantalla independientemente de la orientación. Se activa siempre en tablets de 7"+. w600dp (available width) — ancho disponible considerando la orientación actual. Se activa en teléfonos en paisaje donde el ancho puede superar los 600 dp.
UITraitCollection es un objeto de iOS que contiene características del entorno: size classes (horizontal/vertical), escala de pantalla, capacidad force touch, idiom de interfaz de usuario (iPhone/iPad), gama de colores, ajustes de accesibilidad. Se pasa desde UIApplication a través de la jerarquía de vistas y cambia al modificar la orientación o el tamaño de la ventana.
Use Jetpack WindowManager (Android) y UIScreen nativeBounds + traitCollection (iOS). Los dispositivos plegables cambian Size Class al desplegarse. Se recomienda una arquitectura basada en layouts canónicos (list-detail, supporting pane) con suscripción reactiva a cambios de tamaño de ventana sin reiniciar la Activity.
No. Use un único storyboard con variaciones de constraints por Size Class o layout programático mediante UIKit. SwiftUI no requiere storyboards en absoluto. Los storyboards separados para iPad crean duplicación y complican el mantenimiento — es mejor adaptar un solo layout mediante Size Classes.
Canonical layouts son patrones de diseño adaptativo prefabricados de Google: list-detail (lista + detalle), supporting pane (contenido + panel de acciones), feed (tarjetas). Cada patrón admite tres estados (compact/medium/expanded) y está implementado en la biblioteca Material 3 Adaptive mediante ListDetailScaffold y SupportingPaneScaffold.
Resumen
Desarrollaremos una aplicación móvil llave en mano
IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.
Lea también