Adaptive Layout — ett tillvägagångssätt för att bygga gränssnitt där appen använder olika layouter för olika typer av enheter och skärmorienteringar. Till skillnad från responsive design växlar adaptive layout mellan förberedda layouter vid breakpoint-punkter. Artikeln behandlar Size Classes och UITraitCollection i iOS, kvalificerare sw600dp/layout-w600dp i Android och ger praktiska exempel.
Huvudpunkter
Adaptive Layout — en strategi för att bygga gränssnitt där utvecklaren skapar separata layouter för varje enhetstyp och växlar mellan dem baserat på skärmegenskaper. Till skillnad från responsive design, där element flyter smidigt, använder adaptive layout diskreta växlingar: telefon — en layout, surfplatta — en annan, desktop — en tredje.
Apple introducerade Size Classes i iOS 8 (2014) tillsammans med universella appar (universal binaries som fungerar på både iPhone och iPad). Google introducerade smallestWidth (sw
Moderna rekommendationer från Apple HIG och Google Material Design är överens om att adaptive layout är obligatoriskt för appar som stöder mer än en skärmstorlek. Google Play markerar bristen på anpassning för surfplattor som en brist. App Store har inget formellt krav, men iPad-användare förväntar sig en naturlig upplevelse, inte en utsträckt iPhone-app.
Size Classes — iOS-egenskap som definierar tillgängligt utrymme som Compact eller Regular på varje axel. UITraitCollection — iOS-system för att överföra miljöegenskaper genom vyerarkin: storlek, skärm skala, force touch capability, färgomfång (light/dark), accessibility settings.
| Enhet / Orientering | Horizontal | Vertical |
|---|---|---|
| iPhone porträtt (alla modeller) | Compact | Regular |
| iPhone Plus/Pro Max landskap | Regular | Compact |
| iPhone SE / mini landskap | Compact | Compact |
| iPad porträtt | Regular | Regular |
| iPad landskap | Regular | Regular |
| iPad Split View (1/3 skärm) | Compact | Regular |
UITraitCollection skapas av systemet och överförs från UIApplication via UIWindow till varje UIView. När orientering eller fönsterstorlek ändras (Split View) genererar systemet en ny UITraitCollection och anropar traitCollectionDidChange. I SwiftUI spåras ändringar via Environment Values och ritar automatiskt om vyn.
Anpassning via UITraitCollection i UIKit implementeras genom att prenumerera på trait collection-ändringar och bygga om layouten. Exemplet nedan visar en kontrollenhet som växlar mellan en vertikal stack för iPhone och en split-layout för iPad.
// Adaptiv kontrollenhet med 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: sidofält + innehåll
} else {
showCompactLayout() // iPhone: flikfält + 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
}
}Metoden traitCollectionDidChange anropas vid varje ändring av traits, men vi kontrollerar bara horizontalSizeClass — den mest betydelsefulla indikatorn för layoutändring. I iOS 17+ rekommenderas att använda UIViewController.horizontalSizeClass som computed property för reaktiv uppdatering utan att prenumerera på ändringar.
Android tillhandahåller två huvudsakliga breddkvalificerare: sw
| Kvalificerare | Villkor | Enhetsexempel |
|---|---|---|
| layout-sw600dp | Minsta bredd ≥ 600 dp | iPad (768 dp), Pixel C (900 dp) |
| layout-w600dp | Aktuell bredd ≥ 600 dp | iPad i Split View (600 dp), telefon i landskap |
| layout-land | Landskapsorientering | Vilken enhet som helst roterad horisontellt |
| layout-sw720dp | Minsta bredd ≥ 720 dp | iPad Pro (833 dp), Galaxy Tab S9 (800 dp) |
Resurshierarki i Android: res/layout/activity_main.xml (telefon), res/layout-sw600dp/activity_main.xml (7" surfplatta), res/layout-w600dp-land/activity_main.xml (telefon i landskap). Android väljer den mest specifika kvalificeraren som passar aktuell enhetskonfiguration. Om ingen lämplig fil finns används basresursen från res/layout/.
Modern Android-utveckling använder Jetpack WindowManager för att bestämma fönsterstorlek istället för att direkt läsa resurser. Detta är särskilt viktigt för vikbara enheter, där skärmstorleken kan ändras dynamiskt (Galaxy Fold vecklas ut från 6,2" till 7,6").
// Anpassning via 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("Telefon", 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()
}
}Komponenten currentWindowAdaptiveInfo() från biblioteket androidx.window anpassar gränssnittet till vilken enhet som helst: telefon, surfplatta, vikbar, ChromeOS. Breddklasser: COMPACT (0–600 dp), MEDIUM (600–840 dp), EXPANDED (>840 dp). Höjdklasser: COMPACT (0–480 dp), MEDIUM (480–900 dp), EXPANDED (>900 dp).
SwiftUI tillhandahåller inbyggda verktyg för adaptiv layout utan direkt användning av Size Classes. AnyLayout, ViewThatFits och GeometryReader gör det möjligt att bygga gränssnitt som automatiskt anpassar sig till tillgängligt utrymme.
// SwiftUI-anpassning via AnyLayout och 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("Favoriter", systemImage: "star")
Label("Senaste", systemImage: "clock")
Label("Inställningar", systemImage: "gear")
}
.padding()
}
}
// ViewThatFits — automatiskt layoutval
struct SmartLayout: View {
var body: some View {
ViewThatFits {
HStack { // Prioritet 1: horisontell
CardView()
CardView()
}
VStack { // Om det inte får plats — vertikal
CardView()
CardView()
}
}
}
}ViewThatFits — ett kraftfullt SwiftUI-verktyg som automatiskt väljer den första underordnade vyn som får plats i tillgängligt utrymme utan att kapas. Detta eliminerar behovet av att explicit kontrollera Size Classes för enkla adaptiva växlingar. AnyLayout gör det möjligt att ändra layouttyp (HStack/VStack) utan if-else i vykroppen.
Jetpack Compose använder WindowSizeClass (från Material 3 adaptive-biblioteket) och BoxWithConstraints för adaptiv layout. Till skillnad från XML-kvalificerare bestämmer Compose fönsterstorleken vid körning och återuppbygger UI reaktivt vid konfigurationsändringar.
// Jetpack Compose-anpassning via BoxWithConstraints
@Composable
fun AdaptiveList(items: List<String>) {
BoxWithConstraints {
val width = maxWidth
if (width >= 600.dp) {
// Surfplatta: två kolumner (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 {
// Telefon: en kolumn med navigering
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 tillhandahåller färdiga komponenter: ListDetailScaffold, SupportingPaneScaffold, NavigationSuiteScaffold. Dessa komponenter anpassar sig automatiskt till WindowWidthSizeClass — växlar mellan stacked (COMPACT) och side-by-side (MEDIUM/EXPANDED). Utvecklaren behöver bara välja mönster och skicka innehållspaneler.
Vanliga frågor
sw600dp (smallestWidth) — minsta skärmbredd oavsett orientering. Fungerar alltid på 7"+ surfplattor. w600dp (available width) — tillgänglig bredd med hänsyn till aktuell orientering. Fungerar på telefoner i landskap, där bredden kan överstiga 600 dp.
UITraitCollection — ett iOS-objekt som innehåller miljöegenskaper: size classes (horizontal/vertical), display scale, force touch capability, user interface idiom (iPhone/iPad), color gamut, accessibility settings. Det överförs från UIApplication genom vyerarkin och ändras när orientering eller fönsterstorlek ändras.
Använd Jetpack WindowManager (Android) och UIScreen nativeBounds + traitCollection (iOS). Vikbara enheter ändrar Size Class vid utfällning. En arkitektur baserad på kanoniska layouter (list-detail, supporting pane) med reaktiv prenumeration på fönsterstorleksändringar utan omstart av Activity rekommenderas.
Nej. Använd en storyboard med Size Classes begränsningsvariationer eller programmatisk layout via UIKit. SwiftUI kräver inga storyboards alls. Separata storyboards för iPad skapar duplicering och komplicerar underhåll — det är bättre att anpassa en layout via Size Classes.
Canonical layouts — färdiga adaptiva layoutmönster från Google: list-detail (lista + detaljer), supporting pane (innehåll + åtgärdspanel), feed (kort). Varje mönster stöder tre tillstånd (compact/medium/expanded) och är implementerat i Material 3 Adaptive-biblioteket via ListDetailScaffold och SupportingPaneScaffold.
Sammanfattning
Vi utvecklar en mobil applikation nyckelfärdigt
IT Sectr skapar iOS- och Android-applikationer för startups och företag sedan 2017. Vi ger dig råd och föreslår den bästa lösningen.
Läs också