تخصيص تصميم شريط التنقّل

مثال على صورة متحركة تعرض تنسيق تنقّل مخصّصًا على أجهزة iOS

ملاحظة: يستخدم هذا المستند اصطلاحات تسمية Swift للفئات والبروتوكولات وواجهات برمجة التطبيقات.

يمكنك استخدام ميزة تخصيص التنسيق للتحكّم في هيكلية طرق العرض وموضع الشاشة لكلّ من طرق العرض التي توفّرها Google وطرق العرض المخصّصة لمؤسستك.

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

استخدِم هذا الإطار لعرض معلومات مخصّصة عن النشاط التجاري، مثل حالة الطلب أو تعليمات الاستلام، بدقة على الشاشة مع منع تداخل طرق العرض.

طريقة عمل ميزة "تخصيص التنسيق"

يستخدم تخصيص التنسيق نمط تصميم التفويض. بدلاً من رسم مكوّنات واجهة المستخدم أو تحديد موضعها تلقائيًا على الشاشة، تمرّر حزمة تطوير البرامج (SDK) هذه المكوّنات مباشرةً إلى فئة مخصّصة تكتبها أنت: مفوَّض التنسيق.

لتخصيص التنسيق، أنشِئ فئة تتوافق مع بروتوكول NavigationLayoutDelegate ومرِّر مثيلاً إلى طريقة setLayoutDelegate(_:) في GMSMapView. عندما تتغيّر حالة التنقّل، مثل الانتقال من الخريطة الأساسية إلى الاتّجاهات المفصّلة النشطة، تنفّذ حزمة تطوير البرامج (SDK) طرق ردّ الاتصال على المفوَّض وتوفّر مكوّنات واجهة المستخدم المتاحة لتلك الحالة المحدّدة.

لتقديم تجربة مستخدم سلسة ومتكاملة، يضع إطار العمل هذا فصلاً واضحًا للمسؤولية بين تطبيقك وحزمة تطوير البرامج (SDK). استخدِم مفوَّض التنسيق لتخصيص ما يلي:

  • إنشاء هيكلية طرق العرض: اختَر مكوّنات Google وطرق العرض المخصّصة للنشاط التجاري التي تريد إضافتها إلى الشاشة لكل حالة من حالات واجهة المستخدم.
  • تحديد موضع كل عنصر: يمكنك ضبط نقاط الارتساء والهوامش وموضع التنسيق على الشاشة بدقة. تجنَّب تطبيق قيود مخصّصة على العرض أو الارتفاع في مكوّنات Google لأنّ مكوّنات Google تحسب أبعادها الداخلية.
  • تحديد إطار الخريطة الأساسية: حدِّد الحدود المرئية لكاميرا الخريطة باستخدام إحداثيات مكوّن إطار العرض.
  • تحديد طبقات الشاشة: يمكنك تحديد ما إذا كانت طرق العرض المخصّصة ستظهر فوق عناصر التحكّم المضمّنة في Google أو تحتها أو بجانبها.

في الوقت نفسه، لا يمكنك تخصيص مكوّنات Google التالية باستخدام المفوَّض المسؤول عن التنسيق:

  • أبعاد المكوّنات: حجم المكوّنات التي توفّرها Google وأبعادها الداخلية، والتي تحتسبها حزمة تطوير البرامج (SDK) تلقائيًا.
  • شروط التفعيل: عندما تظهر تنبيهات أو طلبات ديناميكية استنادًا إلى بيانات المسار في الوقت الفعلي

مبادئ التنفيذ

عند كتابة مفوَّض تنسيق، يجب مراعاة القواعد التالية لمنع أخطاء التنسيق أو الأعطال أثناء التشغيل:

  • الإعداد التلقائي هو شاشة فارغة: لا تظهر المكوّنات التي توفّرها Google إلا إذا أضافها المفوَّض صراحةً إلى هيكلية طرق العرض وحدّد مواضعها.
  • واجهات برمجة التطبيقات القديمة غير متاحة: لن تكون السمات والطرق التي تتحكّم في نموذج التنسيق القديم المستند إلى الخانات متاحة، وقد لا تعمل على النحو المتوقّع عندما يكون المفوَّض المخصّص نشطًا.
  • عدم تعديل هياكل طرق العرض الداخلية: لا تجرِ أي تغييرات على هيكلية طرق العرض للمكوّنات التي توفّرها Google (مثل بطاقة الاتجاه أو بطاقة الوقت المقدّر للوصول). بما أنّ هياكل طرق العرض الداخلية هذه هي تفاصيل تنفيذ أساسية، فهي عرضة للتغيير في إصدارات حزمة تطوير البرامج (SDK). وقد يؤدي تعديلها إلى تعطُّل التنسيق أثناء تحديثات حزمة تطوير البرامج (SDK) المستقبلية.

التوافق مع واجهة برمجة التطبيقات القديمة

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

عرض واجهات برمجة التطبيقات القديمة المستندة إلى الخانات

واجهة برمجة التطبيقات القديمة استبدال Layout Delegate
isNavigationHeaderEnabled اطّلِع على طريقة العرض turnCard في startGuidance(navMap:newState:oldState:).
isNavigationFooterEnabled اطّلِع على طريقة العرض etaCard في startGuidance(navMap:newState:oldState:).
isNavigationTripProgressBarEnabled اطّلِع على طريقة العرض tripProgressBar في startGuidance(navMap:newState:oldState:).
isNavigationReportIncidentButtonEnabled ابحث عن الزر .reporting في مصفوفة buttons.
‫setHeaderAccessoryView(_:) وinvalidateLayout(forAccessoryView:) أضِف طريقة العرض الخاصة بك إلى التنسيق وثبِّتها في turnCard.
padding وpaddingAdjustmentBehavior اضبط عرض viewport لتأطير الكاميرا.
navigationHeaderLayoutGuide يمكنك تثبيت طرق العرض المخصّصة مباشرةً في turnCard.
navigationFooterLayoutGuide يمكنك تثبيت طرق العرض المخصّصة مباشرةً في etaCard.
bottomTrailingButtonsLayoutGuide ثبِّت الأزرار مباشرةً على etaCard أو أدلة الحاويات.
bottomTrailingButtonsAlternateLayoutGuide ثبِّت الأزرار مباشرةً على زر .reporting أو قيود wideMode.
‫navigatorWillPresentPrompt(_:) وnavigatorDidDismissPrompt(_:) يمكنك الاعتماد على عمليات الاستدعاء الفوري لـ NavigationLayoutDelegate، مثل showPrompt(navMap:newPrompt:).

عند ضبط مفوَّض تنسيق مخصّص، لن يكون لأدلة التنسيق القديمة (مثل navigationHeaderLayoutGuide أو bottomTrailingButtonsLayoutGuide) وطرق عرض ملحقات العناوين أي تأثير. بدلاً من ذلك، يمكنك ربط طرق العرض بطرق العرض التي توفّرها Google.

لا تزال السمة compassButton في GMSUISettings تتحكّم في البوصلة عندما تكون واجهة مستخدم التنقّل غير مفعَّلة. أثناء تفعيل التنقّل، يقرّر المفوَّض ما إذا كان سيتم عرض البوصلة من مصفوفة buttons.

قائمة التحقّق من الأساسيات

اتّبِع هذه الخطوات والمتطلبات الأساسية لتنفيذ واجهة برمجة التطبيقات Layout Delegate بنجاح:

  • ضبط المفوَّض قبل تفعيل التنقّل: يمكنك استدعاء setLayoutDelegate(_:) على GMSMapView قبل تفعيل التنقّل باستخدام isNavigationEnabled أو enableNavigation(with:). تعرض هذه الطريقة خطأً إذا كان التنقّل مفعَّلاً أو إذا أنشأت عرض الخريطة لشاشة CarPlay. يحتفظ عرض الخريطة بمرجع قوي للمفوَّض. لاستعادة التنسيق المضمّن، يمكنك تمرير nil أثناء إيقاف التنقّل.

    // Set the layout delegate before you enable navigation.
    do {
      try mapView.setLayoutDelegate(MyLayoutDelegate())
    } catch {
      print("Couldn't set the layout delegate: \(error)")
    }
    mapView.isNavigationEnabled = true
        
  • إرفاق طرق العرض الإلزامية: يجب إضافة شعار Google (googleLogo) و"إطار العرض" (viewport) إلى حاوية التنسيق في جميع حالات التنقّل.

  • الالتزام بالأبعاد الثابتة: تحدّد حزمة تطوير البرامج (SDK) أبعاد العديد من المكوّنات التي توفّرها Google. لا تطبِّق قيودًا مخصّصة على العرض أو الارتفاع على العناصر التالية:

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

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

  • التنظيف عند الخروج من الحالة: في stopGuidance(navMap:newState:oldState:)، أزِل طرق عرض الإرشادات النشطة وأوقِف قيودها قبل استعادة التنسيق الجاهز للتنقّل. في disableNavigationUI(navMap:oldState:)، أزِل جميع طرق العرض وأدلة التنسيق التي أضفتها، بما في ذلك أي طلب على الشاشة. يمنع ذلك تداخل المشاهدات من حالة إلى أخرى.

  • توفير أدوات تسهيل الاستخدام والدعم من اليمين إلى اليسار: تستوفي طرق العرض التي توفّرها Google معايير تسهيل الاستخدام، ولكن المفوَّض مسؤول عن تسهيل استخدام التنسيق العام. لدعم اللغات التي تُكتب من اليمين إلى اليسار، استخدِم نقاط الارتساء في البداية والنهاية بدلاً من نقاط الارتساء في اليمين واليسار.

  • تجنُّب واجهات برمجة التطبيقات القديمة للتنسيق: لا تستخدِم السمات القديمة لتعديل التنسيق في GMSUISettings أو أدلة التنسيق القديمة في GMSMapView عندما يكون المفوَّض المخصّص نشطًا. وقد تتجاهل حزمة تطوير البرامج (SDK) مكالمات عبر ميزة الاتصال السابقة هذه عند إرفاق مفوَّض مخصّص.

حالات واجهة المستخدم ومكوّنات Google

عند حدوث انتقال في حالة التنقّل، تمرِّر حزمة تطوير البرامج (SDK) عنصر NavigationUIState للقراءة فقط إلى دالة ردّ الاتصال المفوَّضة. يجمع هذا العنصر علامات إعدادات التنسيق الحالية مع مكوّنات Google (مثل مثيلات UIView) التي تحتاج إليها لعرض الشاشة.

يدير مفوَّض التنسيق هيكلية طرق العرض في أربع حالات تشغيلية. يوضّح المخطّط البياني التالي للحالة كيفية انتقال حزمة تطوير البرامج (SDK) بين حالات التنقّل وطرق ردّ الاتصال التي تنفّذها على المفوَّض:

مخطّط بياني يوضّح انتقال حالة مراحل نشاط NavigationLayoutDelegate بين حالات "عدم التنقّل" و"جاهز للتنقّل" و"الإرشاد النشط".

تعتمد مكوّنات Google المحدّدة التي يتم توفيرها للمفوَّض على المرحلة الحالية من مراحل التنقّل.

المكوّنات المطلوبة (جميع حالات التنقّل)

بغض النظر عن مرحلة التنقّل الحالية، يجب تضمين مكوّنات Google التالية ووضعها وإبقاؤها مرئية في هيكلية طرق العرض:

  • شعار Google ‏ (googleLogo): يعرض هذا المكوّن شعار "خرائط Google" الإلزامي. إذا كان زر إعادة التوسيط مفعَّلاً، يتحوّل الشعار تلقائيًا إلى هذا الزر عندما يمرّر السائق الخريطة بعيدًا عن مركبته. نتيجةً لذلك، يحدّد موضع الشعار مكان ظهور زر إعادة التوسيط. ننصحك بوضع الشعار في الزاوية السفلية من التنسيق ليتوافق مع توقعات واجهة المستخدم العادية في الخريطة.

  • إطار العرض (viewport): هو UIView غير مرئي يحدّد حدود إطار الكاميرا. يحدّد موضع إطار العرض بدقة المكان الذي تضع فيه حزمة تطوير البرامج (SDK) علامة شيفرون المركبة في المنتصف وترسم خط المسار النشط. اضبط موضع إطار العرض بحيث يغطي المنطقة المفتوحة وغير المحجوبة من شاشتك، مع تجنُّب العناصر المركّبة المعتمة بأمان، مثل البطاقات السفلية المخصّصة.

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

  • ‫buttons: مصفوفة من طرق عرض الإجراءات العائمة التي تم ضبطها للخريطة الأساسية (عادةً ما تكون زرّ "البوصلة" فقط). بما أنّ Google قد تضيف أزرارًا أو تعيد ترتيبها في الإصدارات المستقبلية من حزمة تطوير البرامج (SDK)، تجنَّب الاعتماد على فهارس المصفوفات الثابتة. بدلاً من ذلك، يمكنك تكرار المصفوفة وقراءة نوع كل زر من خلال مقارنة السمة type بالقيمة NavigationButtonKnownType.compass، واستخراج العرض المادي من خلال استدعاء السمة view. يُرجى العِلم أنّ الأزرار المحدّدة في هذه المجموعة تختلف عن تلك المتاحة أثناء الإرشاد النشط.

‫ مكوّنات إرشادية نشطة

عند بدء الاتجاهات المفصّلة، تتيح حزمة تطوير البرامج (SDK) الوصول إلى المجموعة الكاملة من عناصر التحكّم في التنقّل. يتيح لك عنصر ActiveGuidanceUIState الوصول إلى مكوّنات Google التالية:

  • ‫turnCard: بانر العنوان الأساسي الذي يعرض اتجاهات المناورة القادمة وقياسات المسافة وإرشادات المسار ضعه في أعلى التنسيق لإنشاء تسلسل هرمي مألوف للتنقل وتثبيت طرق العرض المخصصة حوله.

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

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

  • ‫speedWidget: عنصر تحكّم عائم لعداد السرعة والحدود القصوى للسرعة المنشورة استنادًا إلى إعدادات واجهة برمجة التطبيقات (السمتَين shouldDisplaySpeedometer وshouldDisplaySpeedLimit في GMSMapView) ومدى توفّر البيانات، يتم تغيير حجم طريقة العرض هذه ديناميكيًا بين أربع حالات مرئية في وقت التشغيل (لا يتم عرض أي شيء، أو السرعة الحالية فقط، أو الحد الأقصى للسرعة فقط، أو كلتا القراءتَين). بما أنّ حجم التطبيق المصغّر يمكن أن يتغيّر بين هذه الأحجام بدون إشعار، يجب دائمًا تثبيت طرق العرض المحيطة باستخدام القيود لكي يتكيّف التنسيق تلقائيًا لمنع التداخلات المكانية.

  • ‫buttons: مصفوفة موسّعة من طرق عرض الإجراءات العائمة لحالة الإرشاد النشطة (تتضمّن عادةً كلاً من البوصلة وزر الإبلاغ عن الحوادث). كما هو الحال في حالة الاستعداد للتنقّل، يمكنك تحديد موقع طرق عرض الأزرار الفردية واستخراجها من خلال فلترة الصفيف حسب type (.compass أو .reporting) واستخراج طريقة العرض الفعلية باستخدام السمة view. ويمكنك بعد ذلك تحديد موضعها بشكل مستقل أو إضافتها إلى GMSNavigationHidingStackView. يرتّب عرض الحزمة هذا الأزرار عموديًا، وعند نفاد المساحة، يخفيها بدءًا من نهاية مصفوفة arrangedSubviews. لا تضبط السمة isHidden على طرق العرض التي يديرها.
ملاحظة: أفضل الممارسات المتعلقة بتأطير الكاميرا يتميز عرض بطاقة الانعطاف بارتفاع ثابت بناءً على NavigationStyleValues.headerNominalHeight، وهو طويل بما يكفي لأي محتوى بطاقة انعطاف. للحفاظ على ثبات إطار الكاميرا، يجب تقييد الجزء العلوي من viewport مباشرةً بالحافة السفلية من turnCard، كما هو موضّح في مثال التنفيذ.

مكوّنات الطلبات الديناميكية

يتم تفعيل الطلبات، مثل تنبيهات الحوادث أو تحذيرات كاميرات السلامة، بشكل مستقل أثناء الإرشاد النشط.

عندما يكون الطلب جاهزًا للعرض، تستدعي حزمة تطوير البرامج (SDK) دالة الاستدعاء showPrompt(navMap:newPrompt:) الخاصة بالمفوَّض وتمرّر طريقة عرض newPrompt مباشرةً. يكون المفوَّض مسؤولاً عن تحديد موضع هذا الطلب بسلاسة فوق التنسيق (عادةً ما يتم تثبيته على الحافة السفلية لحاوية الخريطة).

بما أنّ الطلب الوارد يغطي الجزء السفلي من الشاشة، يجب تعديل التنسيق لمنع تداخل الطلب مع إطار العرض أو شعار Google أو أي أزرار محاذية للأسفل.

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

التعامل مع حجم الشاشة والوضع العريض

للتعامل مع التغييرات في أبعاد الخريطة واتجاه الشاشة، يستخدم مفوَّض التنسيق الميزات التالية:

  • تغيير حجم العرض: يضبط التنسيق كلما تغيّر الحجم الفعلي للخريطة.

  • الوضع العريض: يتم التبديل إلى صيغة تنسيق عريض عندما تكون الخريطة عريضة بما يكفي.

الاستجابة لتغيير حجم العرض

يؤدي عادةً استخدام تنسيقات تقسيم الشاشة وأشرطة التمرير في الحاويات وتدوير الجهاز إلى تفعيل دالة الاستدعاء sizeChanged(navMap:newState:). استخدِم هذه الدالة لإجراء تعديلات عامة على واجهة المستخدم المخصّصة لتتجاوب مع الأجهزة المختلفة. استخدِم sizeChanged(navMap:newState:) لإعادة تحديد موضع العناصر المخصّصة لتناسب نسبة عرض إلى ارتفاع جديدة للشاشة، وتطبيق نقاط توقّف مخصّصة للعرض أو الارتفاع، ورصد متى يؤدي حدث تغيير الحجم إلى تبديل الوضع العريض لتبديل خيارات التنسيق بأمان.

لمحة عن الوضع العريض

يتم تفعيل "الوضع العريض" عندما تكون حاوية الخريطة عريضة بما يكفي لعرض مكوّنات واجهة المستخدم جنبًا إلى جنب.

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

اطّلِع على الأمثلة التالية لتحديد موضع مكوّنات التنسيق في الوضعَين العادي والعريض:

  • وضع "بورتريه" العادي: ضع بطاقة الاتجاه في أعلى الشاشة وبطاقة الوقت المقدّر للوصول في أسفلها.

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

التحقّق من الوضع العريض أثناء عمليات انتقال الحالة

إذا كان التنسيق يتيح استخدام وضع العرض الواسع، عليك تقييم القيمة المنطقية wideMode داخل كل دالة استدعاء لانتقال الحالة، بدلاً من الاعتماد فقط على sizeChanged(navMap:newState:).

تحقَّق من عنصر الحالة أثناء عمليات الاستدعاء، مثل enableNavigationUI(navMap:newState:) وstartGuidance(navMap:newState:oldState:)، للتعامل مع عمليات تشغيل التطبيق الأولية في الوضع الأفقي. يضمن هذا النهج أن يقيّم التنسيق حالة الوضع العريض وينشّط الصيغة الصحيحة للتنسيق العادي أو العريض في اللحظة التي تبدأ فيها حالة تنقّل جديدة.

التوافق مع أسلوب Google

لمساعدة طرق العرض المخصّصة في مطابقة تباعد المكوّنات التي توفّرها Google، توفّر حزمة تطوير البرامج (SDK) الفئة NavigationStyleValues. وتعرض خصائص الفئة القياسات بالنقاط.

على سبيل المثال، لوضع زر مخصّص في الزاوية العلوية من الشاشة مقابل بطاقة الاتجاه، استخدِم NavigationStyleValues.headerTopPadding كهامش علوي للزر. بعد ذلك، تتم محاذاة الزر مع الحافة العلوية لبطاقة الاتجاهات.

تتوفّر القياسات التالية:

  • NavigationStyleValues.buttonHorizontalPadding
  • NavigationStyleValues.buttonVerticalPadding
  • NavigationStyleValues.defaultCornerRadius
  • NavigationStyleValues.elementHorizontalPadding
  • NavigationStyleValues.headerFooterHorizontalPadding
  • NavigationStyleValues.headerNominalHeight
  • NavigationStyleValues.headerTopPadding

تتضمّن طرق العرض التي توفّرها Google أيضًا هوامش مضمّنة. عند تجميع طريقتَي عرض من Google، يجب تقييد إحداهما على layoutMarginsGuide الأخرى لكي لا تتراكم الهوامش. على سبيل المثال، لوضع تطبيق السرعة المصغّر فوق بطاقة الوقت المقدّر للوصول، يجب ربط نقطة الارتكاز السفلية لـ speedWidget بـ etaCard.layoutMarginsGuide.topAnchor.

تخصيص الطلبات

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

طلبات تحريك الصور

لا تعرض حزمة تطوير البرامج (SDK) الطلبات بشكل متحرك. يحدّد المفوَّض كيفية ظهور كل طلب واختفائه:

  • في showPrompt(navMap:newPrompt:)، أضِف الطلب إلى عرض الخريطة في موضع البداية، ثم حرِّكه إلى مكانه. على سبيل المثال، ابدأ الطلب أسفل المنطقة المرئية مباشرةً أو خلف بطاقة سفلية مع محاذاة الحافة العلوية مع الجزء العلوي من البطاقة.
  • في hidePrompt(navMap:oldPrompt:)، يمكنك تحريك الطلب خارج نطاق العرض، ثم إزالته من العرض الفائق عند انتهاء الحركة.
  • في changePrompt(navMap:newPrompt:oldPrompt:)، يمكنك تشغيل الصور المتحركة للإخفاء والإظهار في الوقت نفسه لاستبدال طلب بآخر.

يستخدم مثال التنفيذ هذا الأسلوب.

لإيقاف الطلبات المتعلقة بحركة المرور والمسارات الأفضل والحوادث، اضبط السمة shouldDisplayPrompts في GMSNavigator على false.

تقريب زوايا الطلبات

تتوافق الطلبات مع بروتوكول NavigationRoundedCornerView، لذا يمكنك اختيار الزوايا التي تريد تقريبها. By default, a prompt rounds only its top corners, which suits a prompt that's anchored to the bottom edge of the screen. إذا وضعت طلبًا فوق محتوى آخر، مثل بطاقة سفلية، اجعل جميع الزوايا الأربع مستديرة:

newPrompt.roundedCorners = [
  .layerMinXMinYCorner, .layerMaxXMinYCorner,
  .layerMinXMaxYCorner, .layerMaxXMaxYCorner,
]

مثال: تنفيذ التنسيق المستند إلى القيود

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

import GoogleNavigation
import UIKit

/// A layout delegate that positions the essential navigation UI elements: the
/// turn card, ETA card, buttons, Google logo, and viewport. It also slides
/// prompts on and off the screen.
@MainActor
final class BasicLayoutDelegate: NSObject, NavigationLayoutDelegate {
  /// How long prompts take to slide on or off the screen, in seconds.
  private static let promptAnimationDuration: TimeInterval = 0.4

  /// The views and constraints for one navigation state.
  @MainActor
  private final class StateElements {
    let buttonStack = GMSNavigationHidingStackView(
      position: .bottom, alignment: .trailing)
    var constraints: [NSLayoutConstraint] = []
    private unowned let navMap: GMSMapView
    private let buttons: [UIView]
    private let topLevelViews: [UIView]

    init(
      navMap: GMSMapView, buttons: [NavigationUIButton],
      topLevelViews: [UIView]
    ) {
      self.navMap = navMap
      self.buttons = buttons.map { $0.view }
      self.topLevelViews = topLevelViews
    }

    /// Adds the views to the map view, and then activates the constraints.
    func activate() {
      topLevelViews.forEach { navMap.addSubview($0) }
      buttonStack.arrangedSubviews = buttons
      navMap.addSubview(buttonStack)
      NSLayoutConstraint.activate(constraints)
    }

    /// Deactivates the constraints, and then removes the views.
    func deactivate() {
      NSLayoutConstraint.deactivate(constraints)
      buttonStack.removeFromSuperview()
      buttonStack.arrangedSubviews = []
      topLevelViews.forEach { $0.removeFromSuperview() }
    }
  }

  private var navigationReadyElements: StateElements?
  private var activeGuidanceElements: StateElements?
  private var currentPrompt: NavigationPromptView?
  private var promptBottomConstraint: NSLayoutConstraint?

  /// Constraints that apply in both the navigation-ready and active guidance
  /// states.
  private var sharedConstraints: [NSLayoutConstraint] = []

  /// Marks the bottom of the map content area. The guide stays above the ETA
  /// card and above any prompt that's on the screen.
  private let mapContentsBottomGuide = UILayoutGuide()

  // MARK: - Navigation-ready state

  func enableNavigationUI(
    navMap: GMSMapView, newState: NavigationReadyUIState
  ) {
    let safeArea = navMap.safeAreaLayoutGuide
    let bottomGuide = mapContentsBottomGuide
    navMap.addLayoutGuide(bottomGuide)

    // Keep the guide at the bottom of the safe area unless a higher-priority
    // constraint pushes it up.
    let lowPriorityBottom = bottomGuide.bottomAnchor.constraint(
      equalTo: safeArea.bottomAnchor)
    lowPriorityBottom.priority = .defaultLow
    sharedConstraints = [
      bottomGuide.heightAnchor.constraint(equalToConstant: 0),
      lowPriorityBottom,
    ]

    let logo = newState.googleLogo
    let viewport = newState.viewport
    let elements = StateElements(
      navMap: navMap, buttons: newState.buttons,
      topLevelViews: [logo, viewport])
    let buttonStack = elements.buttonStack
    elements.constraints = sharedConstraints + [
      buttonStack.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor),
      buttonStack.topAnchor.constraint(equalTo: safeArea.topAnchor),
      buttonStack.bottomAnchor.constraint(equalTo: bottomGuide.topAnchor),

      logo.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor),
      logo.bottomAnchor.constraint(equalTo: bottomGuide.topAnchor),

      viewport.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor),
      viewport.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor),
      viewport.topAnchor.constraint(equalTo: safeArea.topAnchor),
      viewport.bottomAnchor.constraint(equalTo: bottomGuide.topAnchor),
    ]
    elements.activate()
    navigationReadyElements = elements
  }

  func disableNavigationUI(
    navMap: GMSMapView, oldState: NavigationReadyUIState
  ) {
    currentPrompt?.removeFromSuperview()
    currentPrompt = nil
    promptBottomConstraint = nil
    navigationReadyElements?.deactivate()
    navigationReadyElements = nil
    navMap.removeLayoutGuide(mapContentsBottomGuide)
  }

  // MARK: - Active guidance state

  func startGuidance(
    navMap: GMSMapView, newState: ActiveGuidanceUIState,
    oldState: NavigationReadyUIState
  ) {
    navigationReadyElements?.deactivate()

    let safeArea = navMap.safeAreaLayoutGuide
    let bottomGuide = mapContentsBottomGuide
    let turnCard = newState.turnCard
    let etaCard = newState.etaCard
    let logo = newState.googleLogo
    let viewport = newState.viewport
    let elements = StateElements(
      navMap: navMap, buttons: newState.buttons,
      topLevelViews: [turnCard, etaCard, viewport, logo])
    let buttonStack = elements.buttonStack
    elements.constraints = sharedConstraints + [
      turnCard.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor),
      turnCard.topAnchor.constraint(equalTo: navMap.topAnchor),

      etaCard.leadingAnchor.constraint(equalTo: navMap.leadingAnchor),
      etaCard.bottomAnchor.constraint(equalTo: navMap.bottomAnchor),
      bottomGuide.bottomAnchor.constraint(
        lessThanOrEqualTo: etaCard.topAnchor),

      buttonStack.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor),
      buttonStack.topAnchor.constraint(equalTo: turnCard.bottomAnchor),
      buttonStack.bottomAnchor.constraint(equalTo: bottomGuide.topAnchor),

      logo.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor),
      logo.bottomAnchor.constraint(equalTo: bottomGuide.topAnchor),

      // Frame the camera in the open area below the turn card.
      viewport.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor),
      viewport.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor),
      viewport.topAnchor.constraint(equalTo: turnCard.bottomAnchor),
      viewport.bottomAnchor.constraint(equalTo: bottomGuide.topAnchor),
    ]
    elements.activate()
    activeGuidanceElements = elements
  }

  func stopGuidance(
    navMap: GMSMapView, newState: NavigationReadyUIState,
    oldState: ActiveGuidanceUIState
  ) {
    activeGuidanceElements?.deactivate()
    activeGuidanceElements = nil
    navigationReadyElements?.activate()
  }

  // MARK: - Prompts

  func showPrompt(navMap: GMSMapView, newPrompt: NavigationPromptView) {
    navMap.addSubview(newPrompt)
    currentPrompt = newPrompt

    // Start with the prompt just below the bottom edge of the map view.
    let bottomConstraint = newPrompt.bottomAnchor.constraint(
      equalTo: navMap.bottomAnchor, constant: newPrompt.bounds.height)
    promptBottomConstraint = bottomConstraint
    NSLayoutConstraint.activate([
      bottomConstraint,
      newPrompt.leadingAnchor.constraint(
        equalTo: navMap.safeAreaLayoutGuide.leadingAnchor),
      // Keep the map content above the prompt.
      mapContentsBottomGuide.bottomAnchor.constraint(
        lessThanOrEqualTo: newPrompt.topAnchor),
    ])

    // Lay out the prompt in its starting position, and then slide it up.
    navMap.layoutIfNeeded()
    UIView.animate(withDuration: Self.promptAnimationDuration) {
      bottomConstraint.constant = 0
      navMap.layoutIfNeeded()
    }
  }

  func hidePrompt(navMap: GMSMapView, oldPrompt: NavigationPromptView) {
    guard let bottomConstraint = promptBottomConstraint else { return }
    promptBottomConstraint = nil
    currentPrompt = nil

    // Slide the prompt down, and then remove it.
    UIView.animate(withDuration: Self.promptAnimationDuration) {
      bottomConstraint.constant = oldPrompt.bounds.height
      navMap.layoutIfNeeded()
    } completion: { _ in
      oldPrompt.removeFromSuperview()
    }
  }

  func changePrompt(
    navMap: GMSMapView, newPrompt: NavigationPromptView,
    oldPrompt: NavigationPromptView
  ) {
    // Slide the old prompt out and the new prompt in at the same time.
    hidePrompt(navMap: navMap, oldPrompt: oldPrompt)
    showPrompt(navMap: navMap, newPrompt: newPrompt)
  }
}

لاستخدام المفوَّض، اضبطه في عرض الخريطة قبل تفعيل التنقّل، كما هو موضّح في قائمة التحقّق من الأساسيات.

القيود والمشاكل المعروفة

لا ينطبق تخصيص التنسيق على CarPlay الذي يستخدم نماذج تنسيق ثابتة. إذا طلبت setLayoutDelegate(_:) في عرض خريطة أنشأته لشاشة CarPlay، ستعرض الطريقة خطأً.

لمعرفة المشاكل التي تؤثر في إصدارات معيّنة من حِزم تطوير البرامج (SDK)، يُرجى الاطّلاع على المشاكل المعروفة في ملاحظات الإصدار.