Back home

سلسلة Swift Concurrency 07|المزالق الشائعة عند دمج SwiftUI مع async/await

عادة لا يكون المأزق الحقيقي في بناء الجملة، ولكن في ما إذا كانت "دورة حياة الصفحة" و"دورة حياة المهمة" متوافقة أم لا.

يعد كل من SwiftUI وasync/await أنيقين عند عرضهما بشكل فردي، ولكن عندما يتم دمجهما، فإن المشكلة الأكثر سهولة في الكشف عنها هي دورة الحياة.

وبتعبير أدق هو التفكك بين دورتي الحياة:

  • متى تظهر صفحة SwiftUI أو يُعاد رسمها أو تختفي؟
  • متى تبدأ مهمة غير متزامنة، وتتوقف مؤقتًا، وتنتهي، وتلغى؟

إذا لم تتماشى هذين الأمرين، حتى لو كان من الممكن تشغيل الكود اليوم، فسوف ينمو بسهولة لاحقًا:

  • كرر الطلب
  • وميض الصفحة
  • إعادة كتابة النتائج القديمة
  • حالة التحميل مربكة
  • لا يزال يتم تحديث المهمة بعد مغادرة الصفحة

إذن ما تريد هذه المقالة التحدث عنه حقًا هو: صفحات SwiftUI غير المتزامنة عرضة للفوضى، وما هي الأسباب الجذرية وراء هذه الفوضى.

1. سوء التقدير الأكثر شيوعًا: التعامل مع العرض ككائن مستقر

هذه هي العادة الأسهل للعديد من المطورين الذين لديهم خلفية UIKit.

الجميع سوف الافتراضي:

  • تظهر الصفحة مرة واحدة
  • طلب الإرسال مرة واحدة
  • تحديث الصفحة الحالية بعد عودة الطلب

لكن View في SwiftUI يشبه إلى حد كبير وصف الحالة، وليس مثيلًا مستقرًا يمكن الاحتفاظ به لفترة طويلة.

هذا يعني أنه إذا كان الوضع الافتراضي في ذهنك هو “المنظر الموجود أمامي سيكون موجودًا دائمًا، لذا فإن هذه المهمة تخصه بشكل طبيعي”، فسيكون من السهل التسبب في مشاكل لاحقًا.

لا يتطلب SwiftUI ربط المهام بكائن ثابت، كل ما في الأمر هو أنه من السهل أن تخدع نفسك وتظن أنك قمت بذلك بالفعل.

2. أكبر مأزق في onAppear هو أنه يتم استخدامه كمدخل تهيئة لمرة واحدة.

ستقول العديد من المقالات: قد يتم تنفيذ onAppear عدة مرات. وهذا البيان صحيح، لكنه ليس كافيا.

الخطر الحقيقي هو أنه غالبًا ما يتم كتابته كمدخل قياسي فعلي لـ “تهيئة الصفحة”:

.onAppear {
  Task {
    await loadData()
  }
}

لا تكمن المشكلة في أن هذا الرمز يجب أن يكون خاطئًا، ولكن في أنه من السهل إضافته عقليًا:

“عندما تظهر هذه الصفحة، سيتم تشغيلها مرة واحدة فقط.”

بمجرد أن تفكر بهذه الطريقة، سيظهر ما يلي واحدًا تلو الآخر:

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

لذا فإن الفكرة الأكثر استقرارًا هي: ** دع العملية غير المتزامنة نفسها تكون غير فعالة أو مكررة أو قابلة للاستبدال. **

3. المأزق الثاني: الخلط بين حالة الصفحة وحالة المهمة معًا

تتضمن الحالات الشائعة في صفحة SwiftUI ما يلي:

  • معايير التصفية الحالية
  • محتوى البيانات الحالية
  • هل يتم التحميل؟
  • رسالة خطأ
  • المهام الجارية حاليا

إذا لم تكن هذه الحالات ذات طبقات واضحة، فمن الممكن أن تتجمع معًا بسهولة.

الروائح الكريهة الأكثر شيوعًا هي:

  • items -isLoading
  • error
  • isRefreshing
  • keyword
  • selectedTab

كل قيمة على حدة تبدو منطقية، ولكن من الصعب معرفة مدى ارتباطها ببعضها البعض. ثم ستدخل الصفحة في حالة حرجة للغاية:

  • يبدو أنه في أي حالة
  • لكن لا أحد من الولايات يعبر حقًا عن “ما هي دلالات الصفحة الآن”

في هذه الحالة، بمجرد عودة النتيجة غير المتزامنة، قد تتغير أي حالة، ولن يتم الكشف عن المشكلة إلا عاجلاً أم آجلاً.

4. المأزق الثالث: تتم إعادة كتابة النتائج القديمة إلى واجهة المستخدم الحالية

هذه إحدى المشكلات الأكثر شيوعًا في صفحات SwiftUI غير المتزامنة.

تتضمن السيناريوهات النموذجية ما يلي:

  • يمكن للمستخدمين تبديل علامات التبويب بسرعة
  • كلمات البحث الرئيسية تتغير بشكل مستمر
  • تبديل شروط التصفية بشكل متكرر
  • تقوم الصفحة بتشغيل التحديث والتحميل الأول واحدًا تلو الآخر

ظاهريًا، قد تظن أنك “أرسلت مهامًا متعددة” للتو، ولكن في الواقع، المشكلة الحقيقية هي:

**على الرغم من أن المهمة القديمة لا تزال مكتملة بشكل قانوني، إلا أنها لم تعد تتوافق مع حالة الصفحة الحالية. **

بمجرد أن يظل من الممكن كتابة النتائج القديمة إلى واجهة المستخدم الحالية، يكون المظهر الذي تراه عادةً هو:

  • تومض الصفحة
  • القائمة تتراجع فجأة
  • تنتهي حالة التحميل فجأة
  • تظهر رسالة خطأ لسبب غير مفهوم

تشبه هذه الظواهر إلى حد كبير “الأخطاء الصغيرة”، لكن الأسباب الجذرية متشابهة جدًا: لا تتم إدارة صحة نتائج المهمة.

5. الحفرة الرابعة: نشر كافة الإدخالات غير المتزامنة في العرض

إذا ظهرت هذه الإدخالات على الصفحة في نفس الوقت:

  • onAppear { Task { ... } }
  • refreshable { await ... }
  • onChange(of:) { Task { ... } }
  • انقر فوق الزر لفتح Task آخر

تبدو جميعها مشروعة بشكل فردي، ولكنها مجتمعة سرعان ما تصبح مشكلة:

**علاقة المهمة خارجة عن السيطرة. **

وتميل إلى أن تصبح أصعب وأصعب للإجابة:

  • من هو المدخل الرئيسي
  • من يجب أن يلغي من؟
  • من هو المؤهل لتغيير حالة العرض الحالية؟
  • ما هي جولة المهام التي يتوافق معها تحديث حالة معين؟

لذلك، تحتوي العديد من صفحات SwiftUI غير المتزامنة على عدد كبير جدًا من المداخل، ويمكن لكل مدخل تشغيل المهام مباشرة، وأخيرًا لا توجد طبقة تنسيق موحدة.

6. المأزق الخامس: الافتراضي هو “طالما يمكن تحديث واجهة المستخدم”

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

لكن السؤال الحقيقي ليس “هل سيتم تحديثه؟” ولكن “ما إذا كان مؤهلاً للتحديث في هذا الوقت.”

على سبيل المثال:

  • هل انتهت النتيجة الحالية؟
  • هل الصفحة الحالية لا تزال على قيد الحياة؟
  • هل لا يزال الوضع الحالي مطابقًا لهذه الجولة من المهام؟
  • هل يجب أن يتم التعديل الحالي ضمن دلالات الفاعل الرئيسي؟

إذا لم يتم أخذ هذه الأمور على محمل الجد، فقد لا تتعطل الصفحة على الفور، ولكنها ستتراكم ببطء الكثير من “الارتباك العرضي”.

7. نهج أكثر استقرارًا: اسمح للعرض بتشغيل النية والسماح لطبقة الحالة بإدارة المهمة

طريقة التنظيم التي أفضّلها هي:

  • العرض مسؤول عن التعبير عن نية المستخدم
  • ViewModel أو طبقة الحالة مسؤولة عن إدارة المهام ومؤهلات النتائج
  • العرض يستهلك الحالة التي تم فرزها فقط

ومع ذلك، من الأفضل أن يعرف العرض أقل عن هذه التفاصيل:

  • ما إذا كان سيتم إلغاء المهام القديمة
  • النتيجة التي انتهت صلاحيتها
  • هل التحميل الحالي يتم تحميله لأول مرة أم يتم تحديثه؟
  • ما إذا كانت حالة الخطأ يجب أن تحل محل المحتوى القديم

إذا تركت هذه المشاكل في طريقة العرض، فسوف تتغير الصفحة بسرعة من “واجهة المستخدم التعريفية” إلى “الغلاف التعريفي المغلف بطبقة من المنطق اللامركزي غير المتزامن”.

8. اقتراح أقرب إلى القتال الفعلي

إذا بدأت صفحة SwiftUI في التعقيد، فعادةً ما أجبر نفسي على الإجابة على الأسئلة التالية أولاً:

  1. ما هي إدخالات المهام الموجودة على الصفحة؟
  2. ما إذا كانت المهام المتشابهة تتعايش أو تستبدل أو تتجاهل.
  3. ما هي الحالات التي تمثل الحالات الدلالية للصفحة وما هي الحالات التي تمثل حالات عملية داخلية فقط.
  4. ما إذا كان لا يزال مسموحًا بتغيير النتائج القديمة إلى الصفحة الحالية.
  5. بعد مغادرة الصفحة، ما هي المهام التي يجب الاستمرار فيها وما هي المهام التي يجب إيقافها.

بمجرد عدم تمكنك من الإجابة على هذه الأسئلة، فهذا يعني عادةً أن نموذج مهمة الصفحة لم يتم إنشاؤه بعد.

9. الخلاصة: الصعوبة الحقيقية للصفحات غير المتزامنة في SwiftUI هي محاذاة دورة الحياة

الموقف الشائع هو أن المخاطر الرئيسية بين SwiftUI وasync/await هي بناء الجملة. لكن الوضع الأكثر واقعية هو:

  • View ليس جسمًا مستقرًا كما تظن على السطح.
  • onAppear ليس دلالات تهيئة لمرة واحدة
  • لن تنتهي النتائج القديمة تلقائيًا لأنها “انتهت صلاحيتها”
  • إذا كان هناك عدد كبير جدًا من إدخالات المهام على مستوى الصفحة، فمن المؤكد أنها ستبدأ في الفوضى.

لذا فإن صفحة SwiftUI غير المتزامنة المستقرة حقًا هي:

قم أولاً بمحاذاة دورة حياة الصفحة مع دورة حياة المهمة، ثم اكتب تعليمات برمجية غير متزامنة محددة.

فقط إذا تم تحديد ذلك مسبقًا، فإن خفة SwiftUI وأناقة async/await ستصبح حقًا مزايا، بدلاً من جعل الفوضى أسهل في الكتابة.

FAQ

What to read next

Related

Continue reading