Adaptive Layout هو نهج لتخطيط الواجهات حيث يستخدم التطبيق تخطيطات مختلفة لأنواع مختلفة من الأجهزة واتجاهات الشاشة. على عكس responsive design، يتحول adaptive layout بين التخطيطات المُعدة مسبقًا عند نقاط الفصل. تتناول المقالة Size Classes و UITraitCollection في iOS، ومُؤهلات sw600dp/layout-w600dp في Android مع أمثلة عملية.
أهم النقاط
Adaptive Layout هي استراتيجية لبناء واجهة المستخدم حيث يُنشئ المطور تخطيطات منفصلة لكل نوع جهاز ويتبدل بينها بناءً على خصائص الشاشة. على عكس responsive design حيث تتدفق العناصر بسلاسة، يستخدم adaptive layout تبديلات متقطعة: الهاتف — تخطيط، الجهاز اللوحي — آخر، سطح المكتب — ثالث.
قدمت Apple Size Classes في iOS 8 (2014) مع التطبيقات الشاملة (الثنائيات التي تعمل على 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، نطاق الألوان (فاتح/داكن)، إعدادات إمكانية الوصول.
| الجهاز / الاتجاه | أفقي | عمودي |
|---|---|---|
| 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 وتخطيط مقسم لـ 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: شريط علامات تبويب + دفع
}
}
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 كخاصية محسوبة للتحديث التفاعلي دون اشتراك في التغييرات.
يوفر Android مُؤهلين رئيسيين للعرض: sw
| المُؤهل | الشرط | مثال على الجهاز |
|---|---|---|
| layout-sw600dp | العرض الأدنى ≥ 600 dp | iPad (768 dp), Pixel C (900 dp) |
| layout-w600dp | العرض الحالي ≥ 600 dp | iPad في Split View (600 dp), هاتف في الوضع الأفقي |
| layout-land | اتجاه landscape | أي جهاز تم تدويره أفقيًا |
| 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 تختار تلقائيًا أول عرض فرعي يناسب المساحة المتاحة دون اقتطاع. هذا يلغي الحاجة إلى التحقق صراحة من Size Classes للتبديلات التكيفية البسيطة. AnyLayout يسمح بتبديل نوع التخطيط (HStack/VStack) دون if-else في جسم العرض.
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 — تتبدل بين التكديس (COMPACT) والجانب إلى جانب (MEDIUM/EXPANDED). يحتاج المطور فقط إلى اختيار نمط وتمرير لوحات المحتوى.
الأسئلة الشائعة
sw600dp (smallestWidth) — الحد الأدنى لعرض الشاشة بغض النظر عن الاتجاه. يتم تفعيله دائمًا على الأجهزة اللوحية 7\"+. w600dp (available width) — العرض المتاح مع مراعاة الاتجاه الحالي. يتم تفعيله على الهواتف في الوضع الأفقي حيث قد يتجاوز العرض 600 dp.
UITraitCollection كائن في iOS يحتوي على خصائص البيئة: size classes (أفقي/عمودي)، مقياس الشاشة، قدرة force touch، idiom واجهة المستخدم (iPhone/iPad)، نطاق الألوان، إعدادات إمكانية الوصول. يُمرر من UIApplication عبر تسلسل الـ view ويتغير عند تغيير الاتجاه أو حجم النافذة.
استخدم Jetpack WindowManager (Android) و UIScreen nativeBounds + traitCollection (iOS). تغير الأجهزة القابلة للطي Size Class عند الفتح. يُوصى ببنية تعتمد على التخطيطات الأساسية (list-detail, supporting pane) مع اشتراك تفاعلي في تغييرات حجم النافذة دون إعادة تشغيل Activity.
لا. استخدم storyboard واحد مع تنويعات constraints حسب Size Class أو تخطيط برمجي عبر 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. سوف نقدم لك النصح ونقترح أفضل حل.