تخطي حدود التطبيق: هندسة iOS Live Activities و Dynamic Islands في الوقت الفعلي باستخدام Expo
في هذه التدوينة، أشارككم رحلتي المعمارية وإنجازاتي العملية في بناء iOS Live Activities و Dynamic Islands في الوقت الفعلي ضمن مسار عمل Expo المدار (managed pipeline). تعرف على كيفية الربط بين بيئة تشغيل React Native و ActivityKit الخاصة بلغة Swift دون الحاجة إلى عمل eject، باستخدام config plugins مخصصة ومصافحات push token فعالة.

لسنوات، كانت حدود تطبيق React Native مرسومة بوضوح عند حواف شاشة تطبيقك قيد التشغيل. إذا سحب المستخدم الشاشة للعودة إلى الصفحة الرئيسية، فإن خيط واجهة المستخدم (UI thread) يذهب للنوم، ويتضاءل اتصالك بالمستخدم إلى إشعارات دفع (push notifications) ثابتة. ولكن مع إطلاق نظام iOS 16.1، فتحت Apple مساحة ذات قيمة عالية: Live Activities و Dynamic Island.
عندما قررنا تقديم تتبع فوري وسريع في منتجنا المعتمد على Expo، كانت الحكمة الشائعة تقول: "عليك عمل eject والذهاب إلى React Native المجرد. لا يمكن لـ Expo التعامل مع أهداف native widget extension بشكل نظيف."
أكتب هذا لأخبركم أن هذه الحكمة الشائعة خاطئة. لا يقتصر الأمر على أنه يمكنك بناء Live Activities غنية وتعمل في الوقت الفعلي ضمن بيئة Expo المدارة فحسب، بل يمكنك أيضًا الحفاظ على مسار بناء EAS نظيف وبأمر واحد فقط.
إليكم كيف قمت بهندسة الجسر الخاص بنا لتخطي حدود التطبيق، والعقبات المعمارية التي تجاوزناها، والكود الذي تحتاجه للقيام بذلك بنفسك.
المخطط المعماري (The Architectural Blueprint)
لجعل Live Activities تعمل، يتعين علينا الربط بين نموذجين مختلفين تمامًا:
- بيئة تشغيل JavaScript/React Native: حيث يعيش منطق الأعمال (business logic)، والحالة (state)، واستدعاءات API (polling) أو websockets.
- امتداد iOS Widget Extension: هدف أصيل (native target) خفيف الوزن ومحمي للغاية (highly sandboxed) مكتوب بلغتي Swift و SwiftUI. لا يقوم بتشغيل JS، بل يعمل على نموذج رندر يعتمد على خط زمني صارم (timeline-rendering model) أو يتفاعل مع حمولات دفع APNs البعيدة.
إليك كيف تتدفق البيانات في بنيتنا المعمارية:
+----------------------+ Expo Bridge +--------------------------+
| React Native Engine | ------------------------> | ActivityKit Native Module|
| (TypeScript App) | <------------------------ | (Manages Lifecycle & Tok)|
+----------------------+ +--------------------------+
|
APNs v
+----------------------+ +--------------------------+
| Our Backend | =======================> | iOS Dynamic Island / |
| (Node/Go Service) | (High-Priority Push) | Live Activity UI (Swift) |
+----------------------+ +--------------------------+
هناك طريقتان لتحديث Live Activity: محليًا (عبر طريقة native bridge من JS) وعن بُعد (عبر APNs). إذا كان تطبيقك في الخلفية، فسيتم تقييد التحديثات المحلية أو تعليقها. لذلك، من أجل تتبع حقيقي في الوقت الفعلي (مثل توصيل الطلبات أو مشاركة الرحلات)، فإن التحديثات المدفوعة عبر APNs لا غنى عنها.
الخطوة 1: سحر Expo Config Plugins
التحدي الأكبر في Expo هو أن Widget Extension هو هدف منفصل في مشروع Xcode الناتج. لتجنب التعديل اليدوي لمجلد ios/، يجب علينا كتابة Expo Config Plugin لإنشاء هذا الهدف، ونسخ كود Swift الخاص بنا، وتكوين ملف Info.plist، وربط أطر العمل (frameworks) اللازمة خلال مرحلة الـ prebuild.
إليك نظرة مبسطة على كيفية هيكلة الـ config plugin المخصص لدينا (plugins/withLiveActivities.js):
باستخدام هذا المكون الإضافي، عندما يستدعي نظام الـ CI/CD لدينا الأمر eas build، يقوم Expo تلقائيًا بتكوين مساحة عمل Xcode بهيكل متعدد الأهداف. لا يتطلب الأمر أي نقرات يدوية داخل Xcode.
الخطوة 2: تصميم عقد Swift (السمات وواجهة المستخدم)
نحتاج إلى تحديد "العقد" أو شكل البيانات التي سيرسلها كل من جانب React Native وخادم APNs. في Swift، يتم ذلك عن طريق تحديد هيكل ActivityAttributes.
قم بإنشاء ios/LiveActivityWidget/LiveActivityWidget.swift:
ضع في اعتبارك أن Dynamic Island لها مواصفات هندسية صارمة. صمم واجهاتك المدمجة (compact) والدنيا (minimal) بشكل دفاعي؛ فإذا كان النص طويلاً جدًا، سيقوم نظام iOS بقصه بلا رحمة.
الخطوة 3: ربط React Native بـ ActivityKit
الآن، كيف نقوم بتشغيل هذا من JavaScript؟ يجب أن نكتب Swift Native Module للتفاعل مع ActivityKit. يجب أن يتعامل هذا الموديل مع بدء النشاط، وتمرير الحالة الأولية، وإرجاع Push Token إلى React Native حتى يتمكن نظامنا الخلفي (backend) من إرسال التحديثات المباشرة.
إليك الجوهر الأساسي لجسر Swift Native Module الخاص بنا (ios/ActivityBridge/ActivityBridge.swift):
على جانب TypeScript، نقوم باستيراد هذا الموديول والاستماع إلى الحدث. بمجرد استلام push token، نرسله مباشرة إلى خادم الـ API الخاص بنا:
الخطوة 4: دفع التحديثات في الوقت الفعلي من النظام الخلفي (Backend)
عندما تستهدف Live Activities عبر APNs، فإنك لا ترسل حمولة إشعار دفع قياسية. يجب عليك تنسيقها بدقة، واستهداف ترويسة apns-push-type كـ liveactivity ومطابقة هيكل JSON لـ ContentState في Swift تمامًا.
إليك تمثيل خام للحمولة التي يجب أن يرسلها backend الخاص بك (المكتوب بـ Node/Go) إلى نقطة نهاية APNs الخاصة بـ Apple:
تنبيه حاسم (Crucial Gotcha): ترويسات HTTP لا تقل أهمية عن الحمولة نفسها. عند كتابة كود الإرسال الخاص بالـ backend، تأكد من تعيين:
apns-push-type:liveactivityapns-topic:<Your-App-Bundle-ID>.push-type.liveactivities(لاحظ اللاحقة!)apns-priority:10(إذا كنت تريد التنفيذ الفوري دون تقييد من النظام)
دروس مستفادة بصعوبة من مرحلة الإنتاج
إذا قررت تطبيق هذه البنية المعمارية، فهناك بعض العقبات التي تعلمتها بالطريقة الصعبة. وفر على نفسك ساعات من تتبع الأخطاء (debugging):
- حد الـ 4 كيلوبايت للحمولة (4KB Payload Limit): تحديثاتك عن بعد لها حد أقصى صارم يبلغ 4 كيلوبايت. لا تقم بتمرير سلاسل صور base64 كبيرة أو أشجار JSON ضخمة إلى Live Activity. حافظ على حمولات خفيفة وبسيطة للغاية.
- مصافحة الصور (Image Handshakes): إذا كنت بحاجة إلى عرض صور رمزية ديناميكية للمستخدم أو صور مصغرة للخرائط، فلا يمكنك إرسالها عبر حمولة الدفع. بدلاً من ذلك، اكتبها في حاوية مشتركة لمجموعة التطبيقات (App Group shared container) مثل (
UserDefaults/ مجلد المجموعة المشترك) من كود React Native الخاص بك عندما يكون التطبيق نشطًا، واقرأها داخل Swift باستخدام رندر واجهة مخصص. - دورة حياة Push Token: إن الـ push tokens مؤقتة (ephemeral). إذا قام المستخدم بإغلاق التطبيق بالقوة، أو إذا قرر نظام iOS إعادة تدوير الموارد، فسيتم إنشاء توكن جديد. يجب أن يتعامل الـ API الخاص بك مع توكنز متعددة لـ Live Activity واحدة، ودمجها بسلاسة.
- التعامل مع الحالات القديمة (Stale States): احرص دائمًا على توفير
staleDateمنطقي أو دع الـ backend يرسل حدث إنهاء (end). إن ترك نشاط توصيل "عالق" على شاشة قفل المستخدم بعد ساعات من اكتمال التوصيل هو أسرع طريقة لحذف تطبيقك.
خاتمة
من خلال الاستفادة من Expo Config Plugins للتعامل مع توليد الأهداف واستخدام Expo Modules API لإنشاء جسر نظيف ومحدد الأنواع بين Swift و JS، قمنا ببناء بنية معمارية لـ Live Activity جاهزة للإنتاج دون التضحية بتجربة المطور (DX) لمسار عملنا المدار (managed workflow).
لم نضطر إلى التضحية بدورات التطوير السريعة لدينا، ولم نضطر إلى إدارة إعدادات Xcode المخصصة داخل Git. لقد توسعت حدود التطبيق حقًا، وأصبح الحفاظ على تفاعل مستخدميك مجرد مسألة تتعلق بهياكل Swift المنظمة ومصافحات دفع عالية الأولوية.