EduFlow
أساسيات Swift لتطبيقات iOS
⁦0%⁩

الوحدة 3 · بناء الشاشات بـ SwiftUI

الـ views والـ modifiers

17 دقراءة 4 داختر بحكمة

الدرس في لمحة

ابنِ الـ views في SwiftUI على شكل structs صغيرة، ورتّبها بالـ stacks، ونسّقها بالـ modifiers بالترتيب الصحيح، وتحقّق منها فورًا عبر الـ previews.

  1. 1الـ view هو struct له body
  2. 2الـ stacks والـ spacers
  3. 3الـ modifiers تغلّف الـ views
  4. 4المنطق داخل body
  5. 5الـ previews
  6. 6views صغيرة تُركَّب معًا

لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.

إن كنت قد استخدمت UIKit، أو تعاملت مع الـ DOM يدويًا، فأنت معتاد على أن تخبر الشاشة بما يجب أن تغيّره: اضبط نص هذا العنوان، وأخفِ ذلك الزر، وأدرج صفًا. أما SwiftUI فيقلب ذلك. أنت تصف كيف يجب أن تبدو الشاشة للبيانات الحالية، وSwiftUI يكتشف ما تغيّر ويحدّث البكسلات. تنحصر مهمّتك في كتابة هذا الوصف جيدًا.

الـ view هو struct له body

هذا هو الصف الذي يعرضه Pocket Budget لكل مصروف، باستخدام النوعين Expense وCategory من القسم السابق:

ExpenseRow.swift
import SwiftUI

struct ExpenseRow: View {
    let expense: Expense

    var body: some View {
        HStack(spacing: 12) {
            Image(systemName: expense.category.symbolName)
                .font(.title3)
                .frame(width: 32)
                .foregroundStyle(.tint)

            VStack(alignment: .leading, spacing: 2) {
                Text(expense.title)
                    .font(.headline)
                Text(expense.date, format: .dateTime.day().month())
                    .font(.subheadline)
                    .foregroundStyle(.secondary)
            }

            Spacer()

            Text(expense.amount, format: .currency(code: "USD"))
                .font(.body.monospacedDigit())
        }
        .padding(.vertical, 4)
    }
}

يتوافق ExpenseRow مع الـ protocol المسمّى View، ومتطلّبه الوحيد هو body. إنه struct، فإنشاؤه رخيص، وSwiftUI ينشئ هذه الـ structs ويتخلّص منها باستمرار. لا تفكّر في الـ view (العرض) على أنه الشيء الظاهر على الشاشة؛ فكّر فيه على أنه وصفة يقرؤها SwiftUI كلما تغيّرت البيانات.

some View هو النوع المُعتِم (opaque type) من درس الـ protocols. النوع الحقيقي لهذا الـ body هو generic متداخل بعمق، مبني من كل stack وكل modifier، وsome View تتيح للـ compiler أن يتتبّعه دون أن تكتبه أنت.

الـ stacks والـ spacers

ثلاث حاويات تتكفّل بمعظم التخطيطات. HStack يضع العناصر الأبناء من اليسار إلى اليمين (ومن اليمين إلى اليسار في العربية والعبرية، تلقائيًا)، وVStack من الأعلى إلى الأسفل، وZStack يرتّبها في طبقات من الأمام إلى الخلف. وتتحكّم alignment وspacing في اصطفاف الأبناء. أما Spacer() فيتمدّد ليملأ المساحة الفارغة، وهكذا ينتهي المبلغ مثبّتًا عند الحافة الختامية.

يستطيع Text أن ينسّق القيم لك: Text(expense.amount, format: .currency(code: "USD")) وText(expense.date, format: .dateTime.day().month()) يتبعان الإعدادات المحلية للمستخدم، فيرى المستخدم في القاهرة أو برلين الفواصل وترتيب التاريخ اللذين يتوقّعهما. و.monospacedDigit() يُبقي أعمدة المبالغ مصطفّة.

الـ modifiers تغلّف الـ views

.font(.headline) لا يغيّر النص في مكانه. بل يعيد view جديدًا يغلّف النص ويطبّق عليه خطًّا. اربط ثلاثة modifiers (معدِّلات) وتكون قد بنيت ثلاث طبقات. لهذا الترتيب مهم:

Swift
Text("Food")
    .padding()
    .background(.blue)      // blue covers the text AND the padding

Text("Food")
    .background(.blue)      // blue covers only the text
    .padding()              // clear space outside the blue
على اليسار: الحشوة أولًا ثم الخلفية، فتحيط الطبقة الزرقاء بالنص وحشوته معًا. على اليمين: الخلفية أولًا ثم الحشوة، فتلتصق الطبقة الزرقاء بالنص وتكون الحشوة خارجها. .padding().background(.blue) Food الأزرق يشمل الحشوة .background(.blue).padding() Food الحشوة خارج الأزرق
كل modifier يغلّف الـ view الذي قبله

اقرأ سلسلة الـ modifiers من الأعلى إلى الأسفل على أنها «خذ هذا، ثم غلّفه بذاك». بعض الـ modifiers، مثل .font و.foregroundStyle، تنتقل إلى كل الأبناء، ولهذا يؤدّي ضبط .font(.title3) على VStack إلى تنسيق كل النصوص بداخله.

المنطق داخل body

لأن body وصف، فكود Swift العادي هو ما يقرّر محتواه. جملة if تضمّن view فقط حين يتحقّق شرط، وswitch تختار بين views:

Swift
HStack {
    Text(expense.title)
    if expense.amount > 100 {
        Image(systemName: "exclamationmark.circle")
            .foregroundStyle(.orange)
            .accessibilityLabel("Large expense")
    }
}

لا يوجد استدعاء «أظهِر» أو «أخفِ» في أي مكان. حين يتغيّر المبلغ، يقرأ SwiftUI الـ body من جديد فتظهر الأيقونة أو تختفي من تلقاء نفسها. أبقِ الـ body خاليًا من الآثار الجانبية، مثل استدعاءات الشبكة أو الطباعة، لأن SwiftUI قد يقرؤه مرات كثيرة وفي لحظات لا تتحكّم فيها.

الـ previews

أضف preview (معاينة) في أسفل الملف:

Swift
#Preview {
    ExpenseRow(expense: Expense(title: "Groceries", amount: 64.2, category: .food, date: .now))
        .padding()
}

يرسمها Xcode في الـ canvas بجانب كودك ويحدّثها وأنت تكتب. الـ previews هي أسرع حلقة تغذية راجعة في تطوير iOS. أضف واحدة لكل حالة مثيرة للاهتمام، مثل عنوان طويل أو مبلغ كبير أو تخطيط من اليمين إلى اليسار، وستكتشف أخطاء التخطيط قبل أن تشغّل الـ simulator أصلًا.

views صغيرة تُركَّب معًا

حين يتجاوز الـ body نحو شاشة كاملة، استخرج أجزاءه إلى structs مستقلّة:

Swift
struct CategoryBadge: View {
    let category: Category

    var body: some View {
        Text(category.title)
            .font(.caption.bold())
            .padding(.horizontal, 10)
            .padding(.vertical, 4)
            .background(.blue.opacity(0.15), in: Capsule())
            .foregroundStyle(.blue)
    }
}

الآن يمكن أن يظهر CategoryBadge(category: expense.category) في الصف وفي شاشة التفاصيل وفي شريط الفلاتر، بالتنسيق نفسه تمامًا. الاستخراج مجاني وقت التشغيل، ويعطي SwiftUI وحدات أصغر يقارنها حين تتغيّر البيانات.

كل ما في هذا الدرس يعرض بيانات ثابتة. أما الشاشات الحقيقية فتتغيّر: المستخدم يكتب ويبدّل المفاتيح ويضيف المصاريف. يقدّم الدرس التالي الـ state، أي البيانات التي تقود هذه التغييرات، وكيف يعرف SwiftUI متى يعيد الرسم.

أهم النقاط

  • الـ view في SwiftUI هو struct خفيف يصف الـ body الخاص به كيف يجب أن تبدو الشاشة للبيانات الحالية.
  • VStack وHStack وZStack ترتّب العناصر الأبناء عموديًا وأفقيًا وفي طبقات؛ وSpacer يدفع المحتوى بعيدًا عن بعضه.
  • كل modifier يغلّف الـ view في view جديد، لذلك الترتيب مهم؛ وpadding قبل background يعني أن الخلفية تُرسم خلف الحشوة أيضًا.
  • استخرج subview بمجرّد أن يصعب قراءة الـ body؛ الـ views الصغيرة لا تكلّف شيئًا وقت التشغيل.

قراءات إضافية

تحقّق من فهمك

3 أسئلة

السؤال 1 من 3

كيف يبدو Text("Food").background(.blue).padding()؟

السؤال 2 من 3

لماذا يُعلَن body على أنه some View بدل نوع محدّد؟

السؤال 3 من 3

كبر الـ body لأحد الصفوف حتى صار 70 سطرًا. ماذا يفعل مطوّر SwiftUI متمرّس؟

تدريب

اختر بحكمة

ابنِ شارة التصنيف

يطلب تصميم Pocket Budget عنوان تصنيف على شكل حبّة دواء: خلفية زرقاء على شكل كبسولة، مع إزاحة للنص عن حواف الكبسولة تمنحه مساحة للتنفّس. أيّ سلسلة modifiers تنتج ذلك؟

ابنِ شارة التصنيف