Adaptive Layout — μια προσέγγιση στη σχεδίαση διεπαφών όπου η εφαρμογή χρησιμοποιεί διαφορετικές διατάξεις για διαφορετικούς τύπους συσκευών και προσανατολισμούς οθόνης. Σε αντίθεση με το responsive design, το adaptive layout εναλλάσσεται μεταξύ προκαθορισμένων διατάξεων σε σημεία breakpoint. Το άρθρο αναλύει τα Size Classes και το UITraitCollection στο iOS, τα χαρακτηριστικά sw600dp/layout-w600dp στο Android και παρέχει πρακτικά παραδείγματα.
Κύρια σημεία
Adaptive Layout — μια στρατηγική δημιουργίας διεπαφής όπου ο προγραμματιστής δημιουργεί ξεχωριστές διατάξεις για κάθε τύπο συσκευής και εναλλάσσεται μεταξύ τους με βάση τα χαρακτηριστικά της οθόνης. Σε αντίθεση με το responsive design, όπου τα στοιχεία ρέουν ομαλά, το adaptive 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 αναμένουν εγγενή εμπειρία, όχι μια τεντωμένη εφαρμογή 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 και ανακατασκευή της διάταξης. Το παρακάτω παράδειγμα δείχνει έναν ελεγκτή που εναλλάσσεται μεταξύ κάθετης στοίβας για 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: πλευρική γραμμή + περιεχόμενο
} 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 παρέχει ενσωματωμένα εργαλεία για προσαρμοστική διάταξη χωρίς άμεση χρήση 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 — αυτόματη επιλογή διάταξης
struct SmartLayout: View {
var body: some View {
ViewThatFits {
HStack { // Προτεραιότητα 1: οριζόντια
CardView()
CardView()
}
VStack { // Αν δεν χωράει — κάθετη
CardView()
CardView()
}
}
}
}ViewThatFits — ένα ισχυρό εργαλείο SwiftUI που επιλέγει αυτόματα το πρώτο θυγατρικό view που χωράει στον διαθέσιμο χώρο χωρίς αποκοπή. Αυτό εξαλείφει την ανάγκη για ρητό έλεγχο Size Classes για απλές προσαρμοστικές εναλλαγές. AnyLayout επιτρέπει την αλλαγή τύπου διάταξης (HStack/VStack) χωρίς if-else στο σώμα του view.
Jetpack Compose χρησιμοποιεί WindowSizeClass (από τη βιβλιοθήκη Material 3 adaptive) και BoxWithConstraints για προσαρμοστική διάταξη. Σε αντίθεση με τα XML χαρακτηριστικά, το Compose καθορίζει το μέγεθος παραθύρου κατά τον χρόνο εκτέλεσης και ανασυνθέτει αντιδραστικά το 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 κατά το άνοιγμα. Συνιστάται αρχιτεκτονική βασισμένη σε κανονικές διατάξεις (list-detail, supporting pane) με αντιδραστική εγγραφή στις αλλαγές μεγέθους παραθύρου χωρίς επανεκκίνηση του Activity.
Όχι. Χρησιμοποιήστε ένα storyboard με Size Classes μεταβολές constraint ή προγραμματιστική διάταξη μέσω 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. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.
Διαβάστε επίσης