Adaptive Layout — رویکردی به چیدمان واسطها که در آن برنامه از طرحبندیهای مختلف برای انواع دستگاهها و جهتگیریهای صفحه استفاده میکند. بر خلاف responsive design، adaptive layout بین طرحبندیهای از پیش آماده شده در نقاط breakpoint جابجا میشود. مقاله به بررسی Size Classes و UITraitCollection در iOS، واجدشرایطهای sw600dp/layout-w600dp در Android و ارائه مثالهای عملی میپردازد.
نکات اصلی
Adaptive Layout — استراتژی ساخت واسط که در آن توسعهدهنده طرحبندیهای جداگانهای برای هر نوع دستگاه ایجاد میکند و بر اساس ویژگیهای صفحه بین آنها جابجا میشود. بر خلاف responsive design که عناصر به آرامی جریان مییابند، adaptive layout از جابجاییهای گسسته استفاده میکند: تلفن — یک طرح، تبلت — طرح دیگر، دسکتاپ — طرح سوم.
Apple Size Classes را در iOS 8 (2014) همراه با برنامههای universal (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 و بازسازی layout پیادهسازی میشود. مثال زیر یک کنترلر را نشان میدهد که بین stack عمودی برای 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 ابزارهای داخلی برای چیدمان تطبیقی بدون استفاده مستقیم از 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 { // اولویت ۱: افقی
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 اندازه پنجره را در زمان اجرا تعیین میکند و 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 یا 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 از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.