Adaptive Layout — приступ изради интерфејса при ком апликација користи различите макете за различите типове уређаја и оријентације екрана. За разлику од responsive design-а, adaptive layout се пребацује између унапред припремљених layouts у breakpoint тачкама. Чланак обрађује Size Classes и UITraitCollection у iOS-у, квалификаторе sw600dp/layout-w600dp у Android-у и даје практичне примере.
Главно
Adaptive Layout — стратегија изградње интерфејса при којој програмер креира засебне макете за сваки тип уређаја и пребацује се између њих на основу карактеристика екрана. За разлику од responsive design-а, где елементи течно прелазе, adaptive layout користи дискретна пребацивања: телефон — један layout, таблет — други, desktop — трећи.
Apple је увео Size Classes у iOS 8 (2014) заједно са универзалним апликацијама (universal binaries, које раде и на iPhone-у и на iPad-у). Google је представио smallestWidth (sw
Модерне препоруке Apple HIG и Google Material Design се слажу да је adaptive layout обавезан за апликације које подржавају више од једне величине екрана. Google Play означава недостатак адаптације за таблете као недостатак. App Store нема формални захтев, али корисници iPad-а очекују native искуство, а не развучену iPhone апликацију.
Size Classes — карактеристика iOS-а која одређује доступни простор као Compact или Regular по свакој оси. UITraitCollection — систем iOS-а за пренос карактеристика окружења кроз хијерархију view-а: величина, размера екрана, force touch capability, спектар боја (light/dark), accessibility settings.
| Уређај / Оријентација | Horizontal | Vertical |
|---|---|---|
| iPhone портрет (сви модели) | Compact | Regular |
| iPhone Plus/Pro Max пејзаж | Regular | Compact |
| iPhone SE / mini пејзаж | Compact | Compact |
| iPad портрет | Regular | Regular |
| iPad пејзаж | Regular | Regular |
| iPad Split View (1/3 екрана) | Compact | Regular |
UITraitCollection се формира од стране система и преноси се од UIApplication преко UIWindow до сваког UIView-а. При промени оријентације или величине прозора (Split View), систем генерише нови UITraitCollection и позива traitCollectionDidChange. У SwiftUI-ју промене се прате кроз Environment Values и аутоматски прецртавају view.
Адаптација кроз UITraitCollection у UIKit-у се реализује претплатом на промене trait collection-а и преправљањем layout-а. Пример испод показује контролер који се пребацује између вертикалног стека за iPhone и split-макета за iPad.
// Адаптивни контролер са 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: sidebar + content
} else {
showCompactLayout() // iPhone: таб бар + 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
}
}Метод traitCollectionDidChange се позива при свакој промени traits-а, али ми проверавамо само horizontalSizeClass — најзначајнији индикатор за промену макета. У iOS 17+ препоручује се коришћење UIViewController.horizontalSizeClass као computed property за реактивно ажурирање без претплате на промене.
Android пружа два главна квалификатора ширине: sw
| Квалификатор | Услов | Пример уређаја |
|---|---|---|
| layout-sw600dp | Минимална ширина ≥ 600 dp | iPad (768 dp), Pixel C (900 dp) |
| layout-w600dp | Тренутна ширина ≥ 600 dp | iPad у Split View (600 dp), телефон у пејзажу |
| layout-land | Пејзажна оријентација | Било који уређај окренут хоризонтално |
| layout-sw720dp | Минимална ширина ≥ 720 dp | iPad Pro (833 dp), Galaxy Tab S9 (800 dp) |
Хијерархија ресурса у Android-у: res/layout/activity_main.xml (телефон), res/layout-sw600dp/activity_main.xml (таблет 7"), res/layout-w600dp-land/activity_main.xml (телефон у пејзажу). Android бира најспецифичнији квалификатор који одговара тренутној конфигурацији уређаја. У одсуству одговарајућег фајла, користи се основни ресурс из res/layout/.
Модерни Android развој користи Jetpack WindowManager за одређивање величине прозора уместо директног читања ресурса. Ово је посебно важно за склопиве уређаје, где се величина екрана може мењати динамички (Galaxy Fold се расклапа са 6,2" на 7,6").
// Адаптација кроз 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("Телефон", 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()
}
}Компонента currentWindowAdaptiveInfo() из библиотеке androidx.window прилагођава интерфејс било ком уређају: телефон, таблет, склопиви, ChromeOS. Класе ширине: COMPACT (0–600 dp), MEDIUM (600–840 dp), EXPANDED (>840 dp). Класе висине: COMPACT (0–480 dp), MEDIUM (480–900 dp), EXPANDED (>900 dp).
SwiftUI пружа уграђене алате за адаптивни layout без директног коришћења Size Classes-а. AnyLayout, ViewThatFits и GeometryReader омогућавају изградњу интерфејса који се аутоматски прилагођавају доступном простору.
// SwiftUI адаптација кроз AnyLayout и 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("Омиљено", systemImage: "star")
Label("Недавно", systemImage: "clock")
Label("Подешавања", systemImage: "gear")
}
.padding()
}
}
// ViewThatFits — аутоматски избор layout-а
struct SmartLayout: View {
var body: some View {
ViewThatFits {
HStack { // Приоритет 1: хоризонтални
CardView()
CardView()
}
VStack { // Ако не стане — вертикални
CardView()
CardView()
}
}
}
}ViewThatFits — моћан SwiftUI алат који аутоматски бира први подређени view који стаје у доступни простор без сечења. Ово елиминише потребу за експлицитном провером Size Classes-а за једноставна адаптивна пребацивања. AnyLayout омогућава промену типа layout-а (HStack/VStack) без if-else у телу view-а.
Jetpack Compose користи WindowSizeClass (из библиотеке Material 3 adaptive) и BoxWithConstraints за адаптивни изглед. За разлику од XML квалификатора, Compose одређује величину прозора у runtime-у и реактивно прекомпонује UI при промени конфигурације.
// Jetpack Compose адаптација кроз BoxWithConstraints
@Composable
fun AdaptiveList(items: List<String>) {
BoxWithConstraints {
val width = maxWidth
if (width >= 600.dp) {
// Таблет: две колоне (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 {
// Телефон: једна колона са навигацијом
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 пружа готове компоненте: ListDetailScaffold, SupportingPaneScaffold, NavigationSuiteScaffold. Ове компоненте се аутоматски прилагођавају WindowWidthSizeClass-у — пребацују се између stacked (COMPACT) и side-by-side (MEDIUM/EXPANDED). Програмер само треба да изабере образац и проследи панеле са садржајем.
Често постављана питања
sw600dp (smallestWidth) — минимална ширина екрана независно од оријентације. Увек се активира на таблетима 7"+. w600dp (available width) — доступна ширина узимајући у обзир тренутну оријентацију. Активира се на телефонима у пејзажу, где ширина може премашити 600 dp.
UITraitCollection — iOS објекат који садржи карактеристике окружења: size classes (horizontal/vertical), display scale, force touch capability, user interface idiom (iPhone/iPad), color gamut, accessibility settings. Преноси се од UIApplication кроз хијерархију view-а и мења се при промени оријентације или величине прозора.
Користите Jetpack WindowManager (Android) и UIScreen nativeBounds + traitCollection (iOS). Склопиви уређаји мењају Size Class при расклапању. Препоручује се архитектура заснована на канонским layout-има (list-detail, supporting pane) са реактивном претплатом на промене величине прозора без поновног покретања Activity.
Не. Користите један storyboard са Size Classes варијацијама constraint-а или програмски layout кроз UIKit. SwiftUI уопште не захтева storyboards. Засебни storyboards за iPad стварају дуплирање и отежавају одржавање — боље је прилагодити један макет кроз Size Classes.
Canonical layouts — готови обрасци адаптивног изгледа од Google-а: list-detail (листа + детаљи), supporting pane (садржај + панел акција), feed (картице). Сваки образац подржава три стања (compact/medium/expanded) и имплементиран је у библиотеци Material 3 Adaptive кроз ListDetailScaffold и SupportingPaneScaffold.
Закључак
Развићемо мобилну апликацију под кључ
IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође