Adaptive Layout è un approccio al layout delle interfacce in cui l'applicazione utilizza layout diversi per diversi tipi di dispositivi e orientamenti dello schermo. A differenza del responsive design, l'adaptive layout passa da un layout all'altro tra layout predefiniti in punti di breakpoint. Questo articolo analizza Size Classes e UITraitCollection in iOS, i qualificatori sw600dp/layout-w600dp in Android e fornisce esempi pratici.
Punti chiave
Adaptive Layout è una strategia UI in cui lo sviluppatore crea layout separati per ogni tipo di dispositivo e commuta tra di essi in base alle caratteristiche dello schermo. A differenza del responsive design dove gli elementi fluiscono armoniosamente, l'adaptive layout utilizza commutazioni discrete: telefono — un layout, tablet — un altro, desktop — un terzo.
Apple ha introdotto Size Classes in iOS 8 (2014) insieme alle applicazioni universali (binari funzionanti sia su iPhone che su iPad). Google ha presentato il qualificatore smallestWidth (sw
Le linee guida moderne di Apple HIG e Google Material Design concordano che l'adaptive layout è essenziale per le applicazioni che supportano più di una dimensione dello schermo. Google Play segnala la mancanza di adattamento ai tablet come un difetto. L'App Store non ha un requisito formale, ma gli utenti iPad si aspettano un'esperienza nativa, non un'applicazione iPhone allungata.
Size Classes sono una caratteristica iOS che definisce lo spazio disponibile come Compact o Regular su ciascun asse. UITraitCollection è un sistema iOS per passare le caratteristiche ambientali attraverso la gerarchia delle view: dimensione, scala dello schermo, capacità force touch, gamma colori (chiaro/scuro), impostazioni di accessibilità.
| Dispositivo / Orientamento | Orizzontale | Verticale |
|---|---|---|
| iPhone ritratto (tutti i modelli) | Compact | Regular |
| iPhone Plus/Pro Max paesaggio | Regular | Compact |
| iPhone SE / mini paesaggio | Compact | Compact |
| iPad ritratto | Regular | Regular |
| iPad paesaggio | Regular | Regular |
| iPad Split View (1/3 schermo) | Compact | Regular |
UITraitCollection è formato dal sistema e passato da UIApplication attraverso UIWindow a ogni UIView. Quando l'orientamento o la dimensione della finestra (Split View) cambia, il sistema genera un nuovo UITraitCollection e chiama traitCollectionDidChange. In SwiftUI, i cambiamenti vengono tracciati tramite Environment Values e la view viene ridisegnata automaticamente.
L'adattamento tramite UITraitCollection in UIKit è implementato sottoscrivendo i cambiamenti di trait collection e ricostruendo il layout. L'esempio seguente mostra un controller che commuta tra uno stack verticale per iPhone e un layout diviso per iPad.
// Controller adattivo 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 laterale + contenuto
} else {
showCompactLayout() // iPhone: barra delle schede + 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
}
}Il metodo traitCollectionDidChange viene chiamato a qualsiasi cambiamento di traits, ma controlliamo solo horizontalSizeClass — l'indicatore più significativo per il cambio di layout. In iOS 17+ si raccomanda di usare UIViewController.horizontalSizeClass come proprietà calcolata per aggiornamenti reattivi senza sottoscrizione ai cambiamenti.
Android fornisce due qualificatori principali di larghezza: sw
| Qualificatore | Condizione | Esempio di dispositivo |
|---|---|---|
| layout-sw600dp | Larghezza minima ≥ 600 dp | iPad (768 dp), Pixel C (900 dp) |
| layout-w600dp | Larghezza corrente ≥ 600 dp | iPad in Split View (600 dp), telefono in paesaggio |
| layout-land | Orientamento landscape | Qualsiasi dispositivo ruotato orizzontalmente |
| layout-sw720dp | Larghezza minima ≥ 720 dp | iPad Pro (833 dp), Galaxy Tab S9 (800 dp) |
Gerarchia delle risorse in Android: res/layout/activity_main.xml (telefono), res/layout-sw600dp/activity_main.xml (tablet 7"), res/layout-w600dp-land/activity_main.xml (telefono in paesaggio). Android seleziona il qualificatore più specifico che corrisponde alla configurazione corrente del dispositivo. Quando non esiste un file corrispondente, viene utilizzata la risorsa base da res/layout/.
Lo sviluppo Android moderno utilizza Jetpack WindowManager per determinare la dimensione della finestra invece di leggere direttamente le risorse. Questo è particolarmente importante per i dispositivi pieghevoli dove la dimensione dello schermo cambia dinamicamente (Galaxy Fold si apre da 6.2" a 7.6").
// Adattamento tramite 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("Telefono", 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()
}
}Il componente currentWindowAdaptiveInfo() dalla libreria androidx.window adatta l'interfaccia a qualsiasi dispositivo: telefono, tablet, pieghevole, ChromeOS. Classi di larghezza: COMPACT (0–600 dp), MEDIUM (600–840 dp), EXPANDED (>840 dp). Classi di altezza: COMPACT (0–480 dp), MEDIUM (480–900 dp), EXPANDED (>900 dp).
SwiftUI fornisce strumenti integrati per il layout adattivo senza usare direttamente Size Classes. AnyLayout, ViewThatFits e GeometryReader permettono di costruire interfacce che si adattano automaticamente allo spazio disponibile.
// Adattamento SwiftUI tramite AnyLayout e 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("Preferiti", systemImage: "star")
Label("Recenti", systemImage: "clock")
Label("Impostazioni", systemImage: "gear")
}
.padding()
}
}
// ViewThatFits — selezione automatica del layout
struct SmartLayout: View {
var body: some View {
ViewThatFits {
HStack { // Priorità 1: orizzontale
CardView()
CardView()
}
VStack { // Se non ci sta — verticale
CardView()
CardView()
}
}
}
}ViewThatFits è un potente strumento di SwiftUI che seleziona automaticamente la prima view figlia che si adatta allo spazio disponibile senza tagli. Questo elimina la necessità di controllare esplicitamente Size Classes per commutazioni adattive semplici. AnyLayout permette di cambiare il tipo di layout (HStack/VStack) senza if-else nel corpo della view.
Jetpack Compose utilizza WindowSizeClass (dalla libreria Material 3 adaptive) e BoxWithConstraints per il layout adattivo. A differenza dei qualificatori XML, Compose determina la dimensione della finestra in runtime e ricompone l'UI reattivamente ai cambiamenti di configurazione.
// Jetpack Compose adattivo tramite BoxWithConstraints
@Composable
fun AdaptiveList(items: List<String>) {
BoxWithConstraints {
val width = maxWidth
if (width >= 600.dp) {
// Tablet: due colonne (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 {
// Telefono: una colonna con navigazione
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 fornisce componenti pronti: ListDetailScaffold, SupportingPaneScaffold, NavigationSuiteScaffold. Questi componenti si adattano automaticamente a WindowWidthSizeClass — commutando tra impilato (COMPACT) e affiancato (MEDIUM/EXPANDED). Lo sviluppatore deve solo scegliere un pattern e passare i pannelli di contenuto.
Domande frequenti
sw600dp (smallestWidth) — larghezza minima dello schermo indipendentemente dall'orientamento. Si attiva sempre sui tablet 7"+. w600dp (available width) — larghezza disponibile considerando l'orientamento corrente. Si attiva sui telefoni in paesaggio dove la larghezza può superare 600 dp.
UITraitCollection è un oggetto iOS che contiene le caratteristiche ambientali: size classes (orizzontale/verticale), scala dello schermo, capacità force touch, idioma dell'interfaccia utente (iPhone/iPad), gamma colori, impostazioni di accessibilità. Viene passato da UIApplication attraverso la gerarchia delle view e cambia quando cambiano l'orientamento o la dimensione della finestra.
Utilizzare Jetpack WindowManager (Android) e UIScreen nativeBounds + traitCollection (iOS). I dispositivi pieghevoli cambiano Size Class quando vengono aperti. Si raccomanda un'architettura basata su layout canonici (list-detail, supporting pane) con sottoscrizione reattiva ai cambiamenti di dimensione della finestra senza riavviare l'Activity.
No. Utilizzare un unico storyboard con variazioni di constraint per Size Class o layout programmatico tramite UIKit. SwiftUI non richiede affatto storyboard. Storyboard separati per iPad creano duplicazione e complicano la manutenzione — è meglio adattare un unico layout attraverso Size Classes.
Canonical layouts sono pattern di layout adattivo predefiniti di Google: list-detail (lista + dettaglio), supporting pane (contenuto + pannello azioni), feed (schede). Ogni pattern supporta tre stati (compact/medium/expanded) ed è implementato nella libreria Material 3 Adaptive tramite ListDetailScaffold e SupportingPaneScaffold.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche