تخطَّ إلى المحتوى
Bahaa Taha — بهاء طه
Swift و SwiftUI9 دقائق قراءة١٬٢٤٦ كلمة

بناء الودجت في SwiftUI: ما الذي يختلف عن الشاشة

الودجت ليست شاشة أصغر بل لقطة ثابتة يرسمها النظام مسبقًا — ومن هذه الجملة تتفرّع كل الفروق: الحالة، التحديث، التفاعل، وحدود الذاكرة.

Bahaa Taha — بهاء طهMrBebo
شاشة تطبيق مذاكرة تعرض جدولًا زمنيًا يمكن بناء ودجت منه
شاشة تطبيق مذاكرة تعرض جدولًا زمنيًا يمكن بناء ودجت منه

الإجابة المختصرة

الودجت تُبنى في عملية منفصلة تنتج لقطات مؤرّخة ثم تنتهي، بينما الشاشة تعمل حيّة داخل تطبيقك. لذلك لا معنى لـ@State داخلها، ولا تُنفَّذ حسابات وقت العرض، والتحديث يخضع لميزانية يمنحها النظام لا لتوقيتك. ضع كل ما تحتاجه الواجهة داخل Entry، وخزّن الحالة في App Group مشترك.

في هذه الصفحة
  1. ما الفرق الجوهري بين الشاشة والودجت؟
  2. لماذا لا تعمل `@State` داخل الودجت؟
  3. كيف تُبنى اللقطات فعليًا؟
  4. لماذا لا تتحدّث ودجتي في الوقت الذي حدّدته؟
  5. الأحجام والتفاعل
  6. أخطاء شائعة عند أول ودجت
  7. كيف تختبر الودجت وتشخّص مشاكلها؟
  8. الخلاصة

أول مرة تبني فيها ودجت في SwiftUI تكتب View عادية وتتوقع أن تعمل مثل أي شاشة، ثم تكتشف أن الزر لا يستجيب، وأن @State لا يتغير، وأن البيانات ظهرت مرة واحدة ثم تجمّدت. لا شيء من هذا خطأ في الكود. الودجت هي واجهة صغيرة يبنيها تطبيقك مسبقًا على شكل لقطات مؤرّخة يعرضها النظام لاحقًا خارج التطبيق، وليست شاشة مصغّرة تعمل حيّة — وكل الاختلافات تنبع من هذا التعريف الواحد.

هذا المقال يشرح ما الذي يتغيّر فعليًا حين تنتقل من بناء شاشة إلى بناء ودجت، وأين يقع المبتدئون، وكيف تصمم التحديث بحيث يعمل ضمن قيود النظام لا ضدها.

ما الفرق الجوهري بين الشاشة والودجت؟

الشاشة تعمل داخل تطبيقك وهو حيّ في الذاكرة: تُعيد الرسم كلما تغيّرت الحالة، وتستجيب للمس فورًا، ويمكنها فتح شبكة أو قراءة ملف في أي لحظة.

الودجت لا تعمل. إنها تُبنى في عملية منفصلة (Extension) لفترة قصيرة جدًا، تُنتج خلالها مجموعة من اللقطات المؤرّخة، ثم تنتهي العملية تمامًا. ما يراه المستخدم بعد ذلك هو صورة جاهزة رسمها النظام من إحدى تلك اللقطات، لا كودك وهو ينفّذ.

شاشتك تُنفَّذ وهي معروضة. الودجت تُنفَّذ قبل أن تُعرض بوقت طويل، ثم تختفي.

من هذه النقطة تتفرّع كل الفروق العملية:

الجانبداخل التطبيقداخل الودجت
الحالة@State تعمل طبيعيًالا معنى لها بين اللقطات
التفاعلأي إيماءةضغط فقط، عبر Link أو Button بنية معيّنة
الشبكةمتى شئتداخل نافذة زمنية قصيرة أثناء البناء
الرسوم المتحركةكاملةشبه معدومة
التحديثفوريبميزانية يمنحها النظام

لماذا لا تعمل @State داخل الودجت؟

لأن لا وجود لدورة حياة تحتفظ بها. حين تكتب داخل شاشة:

@State private var count = 0
Button("زد") { count += 1 }

تعمل لأن العرض حيّ، والتغيير يستدعي إعادة رسم. داخل الودجت، الودجت الظاهرة صورة، ولا يوجد كود يعمل ليستقبل الضغطة أو يعيد الرسم.

البديل الصحيح: الحالة تعيش خارج الودجت — في UserDefaults مشتركة عبر App Group، أو في ملف، أو في قاعدة بيانات — والودجت تقرأها أثناء بناء اللقطات فقط.

struct Entry: TimelineEntry {
  let date: Date
  let remaining: Int
}

كل ما تحتاجه الواجهة يجب أن يكون داخل Entry. إن اضطررت لحساب شيء وقت العرض، فقد وقعت في المشكلة نفسها من باب آخر.

كيف تُبنى اللقطات فعليًا؟

الشيء الذي يجب فهمه قبل أي شيء آخر هو TimelineProvider بدواله الثلاث:

  1. `placeholder` — شكل الودجت وهي فارغة تمامًا، تُعرض أثناء التحميل وفي معرض الودجات. اجعلها ببيانات وهمية معقولة لا فارغة.
  2. `getSnapshot` — لقطة واحدة سريعة، تُستخدم في المعاينة. يجب أن ترجع فورًا؛ لا تنتظر الشبكة هنا.
  3. `getTimeline` — العمل الحقيقي: ترجع مصفوفة لقطات مؤرّخة وسياسة لما بعد آخرها.
func getTimeline(in context: Context,
                 completion: @escaping (Timeline<Entry>) -> Void) {
  let now = Date()
  let entries = (0..<6).map { i in
    Entry(date: now.addingTimeInterval(Double(i) * 3600),
          remaining: tasksRemaining(at: i))
  }
  completion(Timeline(entries: entries, policy: .after(now.addingTimeInterval(6 * 3600))))
}

الفكرة الأساسية: أنت لا تحدّث الودجت في وقت لاحق، بل تكتب الآن ما ستعرضه لاحقًا. إن كنت تعرف مسبقًا ما سيظهر بعد ساعة — عدّاد تنازلي، موعد قادم، وقت صلاة — فأنشئ لقطة لكل لحظة الآن، ولا تطلب من النظام إيقاظك.

هذا هو النمط الذي تعتمد عليه ودجات تطبيقات المذاكرة والمهام مثل ذاكرني: الجدول معروف، فتُبنى لقطاته دفعة واحدة.

لماذا لا تتحدّث ودجتي في الوقت الذي حدّدته؟

هذا أكثر سؤال يتكرّر، والجواب أن policy طلب لا أمر.

النظام يمنح كل ودجت ميزانية تحديثات يومية يوزّعها حسب استخدام المستخدم للتطبيق، وموقع الودجت على الشاشة، والبطارية. طلبك بالتحديث بعد خمس دقائق قد يُنفَّذ بعد أربعين. لا توجد طريقة مدعومة لتجاوز ذلك، والتصميم الصحيح يفترض التأخير بدل أن يقاومه.

ثلاثة أساليب عملية:

  • اعرض بيانات لا تفسد بالتقادم. «آخر تحديث: 9:20» أصدق من رقم يبدو لحظيًا وليس كذلك.
  • استخدم النصوص الزمنية التلقائية. Text(date, style: .relative) و.timer يتحدّثان في العرض دون أي تحديث للودجت، وهذا يحلّ معظم حالات العدّاد مجانًا.
  • ادفع من التطبيق عند تغيّر حقيقي. WidgetCenter.shared.reloadTimelines(ofKind:) بعد حفظ المستخدم لتغيير أفضل بكثير من استطلاع دوري.

النقطة الثانية تحديدًا توفّر عليك معظم المعركة، لأن الحالة التي يظنها الناس محتاجة لتحديث مستمر هي غالبًا عدّاد، والعدّاد يتحرّك من غير كودك.

الأحجام والتفاعل

الودجت تُعرض بأحجام متعددة، والكود نفسه يخدمها جميعًا:

@Environment(\.widgetFamily) var family

لا تحاول تصغير تصميم الحجم الكبير ليناسب الصغير. الحجم الصغير يجب أن يحمل معلومة واحدة يمكن قراءتها من بعيد؛ محاولة حشر ثلاثة أسطر فيه تنتج ودجت لا تُقرأ في أي حجم.

أما التفاعل فله ثلاثة مستويات:

  1. فتح التطبيق فقط — السلوك الافتراضي، بلا كود إضافي.
  2. فتح شاشة محددة — لُفّ أجزاء الواجهة بـLink بوجهات مختلفة، ثم استقبلها بـonOpenURL.
  3. تنفيذ فعل دون فتح التطبيق — عبر App Intents، وهي الطريقة الوحيدة المدعومة لتنفيذ عمل من داخل الودجت مباشرة.

المستوى الثالث محدود عمدًا: الفعل يجب أن يكون قصيرًا ومحدّدًا (تعليم مهمة كمنجزة، تبديل مفتاح)، لا عملية طويلة.

أخطاء شائعة عند أول ودجت

خمسة أخطاء تتكرر، وكلها تعود إلى معاملة الودجت كشاشة:

  • نسيان App Group. الودجت عملية منفصلة لا ترى UserDefaults.standard الخاصة بالتطبيق. بلا App Group مشترك ستقرأ الودجت فراغًا دائمًا، وهذا سبب «البيانات لا تظهر» الأول.
  • عمل شبكي طويل داخل `getTimeline`. الميزانية الزمنية ضيقة، وتجاوزها يُنهي العملية فتبقى الودجت على لقطتها القديمة. حمّل البيانات في التطبيق، واحفظها، ودع الودجت تقرأ.
  • الاعتماد على `Date()` وقت العرض. الكود لا يعمل وقت العرض؛ التاريخ الذي يهم هو entry.date.
  • صور كبيرة داخل الودجت. حدود الذاكرة صارمة، والصورة غير المصغّرة سبب متكرر لودجت فارغة بلا رسالة خطأ واضحة.
  • افتراض أن الاختبار في المحاكي يمثّل الواقع. التحديثات في المحاكي أسخى بكثير مما تحصل عليه على جهاز حقيقي في الاستخدام العادي.

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

كيف تختبر الودجت وتشخّص مشاكلها؟

تشخيص الودجت أصعب من تشخيص الشاشة لسبب واحد: العملية تنتهي قبل أن تنظر إليها، فلا يوجد شيء لتتصل به بمصحّح الأخطاء بالطريقة المعتادة.

أربع خطوات تختصر معظم الوقت الضائع:

  • ابدأ بمعاينة Xcode. #Preview يدعم أحجام الودجت وسلاسل اللقطات، ويمكنك تمرير عدة Entry لترى كيف تتغيّر الواجهة عبر الزمن دون انتظار النظام.
  • شغّل الودجت كهدف مستقل. اختر مخطط الـ Extension في Xcode وشغّله؛ يتوقف المصحّح عندها داخل getTimeline فعليًا، وهذا ما يكشف أن الدالة لم تُستدعَ أصلًا أو أنها رجعت بلقطات فارغة.
  • سجّل بـ `Logger` لا بـ `print`. مخرجات الطباعة من عملية منفصلة قصيرة العمر تضيع غالبًا، بينما سجلّ النظام يبقى ويمكن تصفيته باسم الـ Extension في تطبيق Console.
  • افحص القراءة من App Group صراحة. اكتب قيمة معروفة من التطبيق واقرأها في الودجت وسجّلها؛ إن كانت nil فالمشكلة في الإعداد لا في الواجهة.

وهناك سلوك يربك المبتدئين ويستحق ذكره: بعد تثبيت الودجت لأول مرة قد تعرض حالة placeholder لثوانٍ قبل أول جدول حقيقي. هذا طبيعي، ولذلك يجب أن تبدو الـ placeholder كودجت مكتملة ببيانات وهمية معقولة، لا كمستطيل فارغ يوحي بأن شيئًا تعطّل.

أخيرًا، قِس استهلاك الذاكرة مبكرًا. الحد المتاح لعملية الودجت أضيق بكثير من التطبيق، وتجاوزه ينهي العملية بلا رسالة مفيدة — وهو ما يظهر للمستخدم كودجت فارغة تمامًا.

الخلاصة

الودجت ليست شاشة أصغر، بل عقد مختلف: أنت تنتج لقطات مؤرّخة مسبقًا، والنظام يقرر متى يعرضها ومتى يسمح لك بإنتاج المزيد.

ابنِ على هذا الأساس — كل ما تحتاجه الواجهة داخل Entry، والحالة في تخزين مشترك عبر App Group، والوقت عبر Text(style:) بدل التحديث، والدفع من التطبيق عند تغيّر حقيقي — وستتصرّف الودجت كما تتوقع. وثائق WidgetKit تغطّي التفاصيل، لكن الجملة التي تختصر الفرق كله هي أن الودجت تُنفَّذ قبل أن تُرى بوقت طويل.

أسئلة شائعة

لماذا تظهر الودجت فارغة رغم وجود بيانات في التطبيق؟
غالبًا لعدم إعداد App Group مشترك. الودجت عملية منفصلة لا ترى UserDefaults.standard الخاصة بالتطبيق، فتقرأ فراغًا دائمًا.
هل يمكن إجبار الودجت على التحديث كل خمس دقائق؟
لا. سياسة التحديث طلب لا أمر، والنظام يوزّع ميزانية يومية حسب الاستخدام والبطارية. استخدم Text(date, style:) للعدّادات، وادفع تحديثًا من التطبيق عند تغيّر حقيقي.
كيف أنفّذ فعلًا دون فتح التطبيق؟
عبر App Intents، وهي الطريقة المدعومة الوحيدة. الفعل يجب أن يكون قصيرًا ومحدّدًا مثل تعليم مهمة كمنجزة، لا عملية طويلة.
هل أختبر الودجت في المحاكي؟
للتخطيط والشكل نعم، لكن التحديثات في المحاكي أسخى بكثير من الجهاز الحقيقي، فسلوك التوقيت لا يُقاس إلا على جهاز في استخدام عادي.

المصادر

  1. WidgetKit documentationApple Developer
  2. Keeping a widget up to dateApple Developer
  3. ذاكرني على App StoreTecno Blocks
Bahaa Taha — بهاء طه

نشرته

MrBebo

محتوى تقني عربي يقدمه بهاء طه حول البرمجة، الذكاء الاصطناعي، Web2 وWeb3، تطوير التطبيقات، بناء المنتجات وأدوات المطورين.

عن المدوّنة

قراءات ذات صلة

تابع القراءة

تصفّح كل شيء