تطوير الويب

بناء أوّل view SwiftUI: views وmodifiers وstate

8 دقائق للقراءة

📘 الدليل الرئيسي: تطوير تطبيق iOS بـ Swift وSwiftUI: بانوراما 2026. قبل هذا الدليل، بيئة Xcode يجب أن تعمل — راجع دليل التثبيت.

أوّل تماسّ مع SwiftUI يُحيِّر أحيانًا: لم نعد نكتب سلسلة تعليمات تُعَدِّل هرمية views، بل نصف view بدلالة الحالة الراهنة. هذا التحوّل في النموذج يتطلّب أمثلة ملموسة للترسّخ. ينطلق هذا الدليل من مشروع فارغ ويبني view SwiftUI كاملة — نموذج ملف شخصي بحقول، slider، زرّ، وقائمة — مع تقديم كلّ مفهوم مفتاحي في لحظة استعماله.

المتطلّبات

  • Xcode 26 مثبَّت على macOS Tahoe.
  • أساسيات Swift (متغيّرات، دوالّ، structs) — راجع دليل أساسيات اللغة.
  • الوقت المتوقّع: 60 إلى 90 دقيقة.
  • المستوى: مبتدئ.

الخطوة 1 — إنشاء مشروع iOS SwiftUI

قالب iOS App في Xcode يُنتج بنقرتَين مشروع SwiftUI وظيفيًّا.

في Xcode: File ← New ← Project، اختر iOS ← App، انقر Next. املأ:

Product Name:            ProfilSwiftUI
Team:                    Apple ID
Organization Identifier: com.example
Interface:               SwiftUI
Language:                Swift
Storage:                 None

احفظ في ~/Developer. Xcode يفتح المشروع على ContentView.swift الذي يحوي سلفًا VStack بأيقونة globe والنصّ « Hello, world! ». هذه view نقطة انطلاقنا.

الخطوة 2 — فهم تشريح View

في SwiftUI، view هي struct تُنَفِّذ بروتوكول View. هذا البروتوكول يتطلّب شيئًا واحدًا: خاصية محسوبة body تُرجع View أخرى. التركيب بالتداخل يُنتج كلّ الهرمية الرسومية.

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack(spacing: 16) {
            Image(systemName: "person.circle.fill")
                .font(.system(size: 80))
                .foregroundStyle(.blue)
            Text("Profil")
                .font(.title)
                .bold()
        }
        .padding()
    }
}

#Preview {
    ContentView()
}

الكلمة some View نوع opaque يقول للمُجَمِّع « هذه الخاصية تُرجع نوعًا ملموسًا مطابقًا لـ View، لكنّ المُستدعي لا يحتاج معرفته ». الكتلة #Preview (macro منذ Xcode 15) تُعلن preview حيّ يظهر في canvas يمين — اضغط ⌥⌘P لإعادة إظهاره.

الخطوة 3 — إضافة state محلّي بـ @State

لتستجيب view لتعديلات بيانات، يجب وَسم هذه البيانات كجزء من الحالة المُراقَبة. الـ wrapper @State يُشير إلى SwiftUI أنّ خاصية جزء من مصدر الحقيقة المحلّي للـ view ويُشَغِّل إعادة عرض عند كلّ تعديل.

struct ContentView: View {
    @State private var nom: String = ""
    @State private var age: Double = 25

    var body: some View {
        VStack(spacing: 24) {
            Text("Profil de \(nom.isEmpty ? "…" : nom)")
                .font(.title2)

            TextField("Votre prénom", text: $nom)
                .textFieldStyle(.roundedBorder)

            HStack {
                Text("Âge : \(Int(age))")
                Slider(value: $age, in: 18...80)
            }
        }
        .padding()
    }
}

اللاحقة $ قبل nom وage تُنشئ binding: مرجع ثنائي الاتّجاه يُتيح للـ TextField وSlider قراءة الخاصية وتعديلها. دون $، لدينا القيمة فقط. هذا التمييز محوريّ في SwiftUI: nom يقرأ القيمة، $nom يُعطي وصولًا لرابط الكتابة. عند التنفيذ، الكتابة في حقل النصّ تُحدِّث العنوان فورًا، وتحريك الـ slider يُحدِّث لصاقة العمر.

الخطوة 4 — Modifiers والتركيب

modifier هو دالّة تُرجع view جديدة مُشتقّة. .padding()، .font(.title)، .foregroundStyle(.red) هي modifiers. الترتيب الذي نُطَبِّقها به مهمّ أحيانًا: .padding().background(.yellow) يرسم خلفية وراء الهوامش، بينما .background(.yellow).padding() يرسم خلفية فقط وراء المحتوى.

Text("Bonjour")
    .font(.title)
    .foregroundStyle(.white)
    .padding(.horizontal, 24)
    .padding(.vertical, 12)
    .background(.blue, in: Capsule())
    .shadow(radius: 4)

هذا المقطع يُنتج نصًّا أبيض على خلفية زرقاء بشكل كبسولة، مع ظلّ خفيف وهوامش داخلية غير متماثلة. الصيغة .background(.blue, in: Capsule()) أكثر مباشرة من المكافئ بـ ZStack. في وضع Selectable في preview Xcode، يمكن النقر على كلّ قطعة لرؤية الكود المقابل — مساعدة تعلّم فعّالة.

الخطوة 5 — التخطيط: VStack وHStack وZStack وGrid

SwiftUI يُوفّر أربعة containers رئيسية. VStack يُكَدِّس عموديًّا، HStack أفقيًّا، ZStack يُرَكِّب في العمق. Grid (منذ iOS 16) يُنتج شبكة محاذاة كجدول. المعامل spacing: يتحكّم في الفجوة بين العناصر، وalignment: يتحكّم في المحاذاة العمودية على المحور.

VStack(alignment: .leading, spacing: 8) {
    Text("Nom").bold()
    Text("Aïssatou Sow")
    Divider()
    HStack(spacing: 12) {
        Image(systemName: "envelope")
        Text("contact@example.com")
    }
    .foregroundStyle(.secondary)
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 12))

maxWidth: .infinity يطلب من الـ view أن تأخذ كلّ العرض المتاح. الـ modifier .background(.regularMaterial, …) يستعمل مادّة شفّافة تتكيّف مع الموضوع الفاتح/الداكن. هذا التخطيط يُنتج بطاقة هويّة متماسكة بصريًّا في سطور قليلة — نتيجة مكافئة لعشرات الأسطر في UIKit بقيود يدوية.

الخطوة 6 — قوائم ديناميكية بـ ForEach وList

لعرض مجموعة، List هي الأداة الصحيحة: تُدير الـ scroll، التنميط الأصلي، الفواصل، وأفعال swipe. للتكرار على مصفوفة، ForEach هو المكافئ الوظيفي لحلقة.

struct Hobby: Identifiable {
    let id = UUID()
    let nom: String
    let icone: String
}

struct ContentView: View {
    @State private var hobbies: [Hobby] = [
        Hobby(nom: "Lecture", icone: "book.fill"),
        Hobby(nom: "Cyclisme", icone: "bicycle"),
        Hobby(nom: "Photographie", icone: "camera.fill")
    ]

    var body: some View {
        List {
            ForEach(hobbies) { h in
                HStack {
                    Image(systemName: h.icone)
                        .foregroundStyle(.blue)
                    Text(h.nom)
                }
            }
            .onDelete { indexSet in
                hobbies.remove(atOffsets: indexSet)
            }
        }
    }
}

البروتوكول Identifiable يتطلّب مُعَرِّفًا فريدًا id؛ UUID() يُولّد جديدًا في كلّ إنشاء. ForEach يستند إلى هذا المُعَرِّف لمزامنة تغييرات القائمة بكفاءة. الـ modifier .onDelete يُضيف إيماءة swipe-للحذف المميِّزة لـ iOS، دون أيّ كود إدارة صريح.

الخطوة 7 — الأزرار والأفعال

Button في SwiftUI يأخذ فعلًا ولصاقة. يُدير مجّانًا الحالات (مضغوط، مُفلَت، مُعَطَّل) والوصول. لتنميط زرّ، نجمع .buttonStyle والـ modifiers الكلاسيكية.

@State private var compteur = 0

Button {
    compteur += 1
} label: {
    HStack {
        Image(systemName: "plus.circle.fill")
        Text("Incrémenter (\(compteur))")
    }
    .padding()
    .frame(maxWidth: .infinity)
    .background(.blue.gradient, in: RoundedRectangle(cornerRadius: 12))
    .foregroundStyle(.white)
}
.disabled(compteur >= 10)

الـ modifier .disabled(compteur >= 10) يُرَمِّد الزرّ ويمنع النقرات حين يتحقّق الشرط. SwiftUI يُطَبِّق تلقائيًّا نمطًا مرئيًّا لحالة معطَّلة دون كود صريح. الصياغة بـ trailing closures ({ … } label: { … }) هي الشكل الرسمي المُقَدَّم مع Swift 5.3 وتبقى الأكثر قراءة للأزرار الغنية.

الخطوة 8 — تمرير binding إلى sub-view

حين تكبر view، نُجَزِّئها إلى sub-views. إن احتاجت sub-view قراءة وتعديل حالة من الـ view الأبّ، تستقبل @Binding بدل قيمة ثابتة. هي آليّة التواصل الصاعد في SwiftUI.

struct SliderAvecLabel: View {
    let titre: String
    @Binding var valeur: Double

    var body: some View {
        VStack(alignment: .leading) {
            Text("\(titre) : \(Int(valeur))")
                .font(.subheadline)
            Slider(value: $valeur, in: 0...100)
        }
    }
}

struct ContentView: View {
    @State private var luminosite: Double = 50

    var body: some View {
        SliderAvecLabel(titre: "Luminosité", valeur: $luminosite)
            .padding()
    }
}

الأبّ يُمَرِّر $luminosite إلى المعامل valeur المَوسوم @Binding. الـ sub-view تقرأ وتكتب في نفس مصدر الحقيقة، دون الحاجة لمعرفة class الأبّ ولا استعمال delegates. هذا النمط يحلّ محلّ delegates وcallbacks الكلاسيكية في UIKit في الغالبية العظمى من الحالات.

الخطوة 9 — الاختبار على simulator

قبل الانتقال إلى الشاشة التالية أو الدليل التالي، تشغيل الـ view على simulator حقيقي يتحقّق من ألّا يختبئ خطأ خلف سحر preview. اختر simulator في شريط Xcode العلوي (مثل iPhone 16 Pro)، ثم انقر زرّ المثلّث أو ⌘R.

عند التشغيل، app تعرض الملف الشخصي المبنيّ خطوة بخطوة. الكتابة في حقل النصّ تُحدِّث العنوان، تحريك الـ slider يُحدِّث اللصاقة، حذف hobby من القائمة يُخفي السطر مع رسوم متحرّكة. إن عمل كلّ شيء، البيئة الكاملة — كود، preview، runtime — مُصادَق عليها.

نظرة سريعة على modifiers الأكثر استعمالًا يوميًّا

بمجرّد استيعاب الآلية الأساسية، بعض modifiers تعود في شبه كلّ view نكتبها. .padding() يُضيف هوامش داخلية ويقبل جانبًا محدّدًا (.horizontal، .top) أو قيمة عددية. .frame(width:height:) يفرض بُعدًا ثابتًا؛ .frame(maxWidth: .infinity) يطلب احتلال كلّ العرض المتاح. .foregroundStyle يُلَوِّن المحتوى وحلّ محلّ .foregroundColor القديم في iOS 17 — قبول التدرّجات والمواد إضافة إلى الألوان السادّة من المكاسب الرئيسية. .background يرسم خلفية وراء الـ view، بشكل اختياري لإنتاج بطاقات أو كبسولات. .cornerRadius يُدَوِّر الزوايا؛ الشكل الحديث .clipShape(RoundedRectangle(cornerRadius: 12)) أكثر مرونة. .shadow يُلقي ظلًّا، قابل للضبط في الشعاع والإزاحة.

وراء التنميط، modifierَان يعودان دومًا للتفاعل والوصول. .onTapGesture يلتقط tap بسيطًا، .onLongPressGesture ضغطًا طويلًا. .accessibilityLabel("Description claire") يستبدل الوصف التلقائي بنصّ صريح لـ VoiceOver — إيماءة تستغرق عشر ثوانٍ وتُغيِّر جذريًّا قابلية الاستعمال للأشخاص غير المبصرين. الانضباط « modifier وصول واحد لكلّ تفاعل غير تافه » هو ما يفصل app مُتَسَرِّع عن app جدّي.

أخطاء شائعة في SwiftUI

الخطأ السبب الحلّ
« Cannot find ‘$nom’ in scope » محاولة استعمال $ على خاصية ليست @State وَسم الخاصية @State، @Binding، أو استعمال @Observable
preview لا يُحدَّث خطأ ترجمة صامت افحص منطقة Issues (⇧⌘5)؛ أعد تشغيل preview بـ ⌥⌘P
« Type ‘X’ does not conform to protocol ‘Identifiable' » عنصر ForEach بلا id طابق النوع مع Identifiable أو مَرِّر id: \\.self إلى ForEach
لوحة المفاتيح تُخفي حقل TextField لا رفع تلقائي غَلِّف في ScrollView أو استعمل .scrollDismissesKeyboard(.interactively)
« The compiler is unable to type-check this expression in reasonable time » body مُعَقَّد في تعبير واحد جَزِّئ إلى sub-views أو خصائص محسوبة
رسم متحرّك ناقص تعديل حالة خارج كتلة withAnimation غَلِّف التعديل: withAnimation { compteur += 1 }

أسئلة شائعة

لماذا body يُرجع some View وليس View؟ بروتوكول View له associated type (Body) يجعله غير قابل للاستعمال كنوع عودة ملموس. some View نوع opaque يقول « نوع ملموس دقيق مطابق لـ View، مخفيّ عن المُستدعي ». يُتيح هذا لـ SwiftUI إنتاج أنواع مُعَقَّدة داخليًّا دون فرض كتابتها.

متى نستعمل @State، @Binding، @Observable؟ @State لحالة محلّية لـ view. @Binding لتقرأ sub-view وتكتب حالة الأبّ. @Observable لنموذج مشترك بين عدّة views.

هل preview Xcode محاكاة صادقة؟ صادقة جدًّا للتخطيط والعرض المرئي. أقلّ صداقة للسلوكيات غير المتزامنة، إشعارات النظام، والرسوم المتحرّكة المعتمدة على frame rate. الاختبار على simulator أو جهاز حقيقي يبقى ضروريًّا قبل اعتبار ميزة منتهية.

ماذا نفعل حين لا يفعل modifier شيئًا؟ ثلاثة مسارات: افحص ترتيب modifiers؛ افحص تطبيقها على view الصحيحة؛ راجع التوثيق الرسمي — بعض modifiers لها آثار سياقية.

أيمكن خلط UIKit وSwiftUI؟ نعم، عبر UIViewRepresentable وUIViewControllerRepresentable لتضمين UIKit في SwiftUI، أو UIHostingController لتضمين SwiftUI في UIKit. هذا الاستجواب يُستعمَل لهجرة app legacy تدريجيًّا.

هل يعمل SwiftUI على iPad وMac؟ نعم، نفس الكود يُنَفَّذ على iPhone وiPad وMac وApple Watch وApple TV وApple Vision Pro مع تكييفات صغيرة (NavigationSplitView للشاشات الكبيرة، modifiers مشروطة بالمنصّة).

الأدلّة الموصى بها بعد هذا

مصادر رسمية

  • توثيق SwiftUI — كتالوج كامل.
  • SwiftUI Tutorials الرسمية — مسار موجَّه من WWDC.
  • مرجع بروتوكول View.
  • SF Symbols — مكتبة الأيقونات الرسمية القابلة للاستعمال عبر Image(systemName:).

🔝 العودة إلى الدليل الرئيسي.

Sponsoriser ce contenu

Cet emplacement est à vous

Position premium en fin d'article — c'est l'instant où les lecteurs sont le plus engagés. Réservez cet espace pour votre marque, votre formation ou votre offre.

Recevoir nos tarifs
Publicité