
אתם יכולים להשתמש בהתאמה אישית של הפריסה כדי לשלוט בהיררכיית התצוגות ובמיקום במסך של התצוגות ש-Google מספקת ושל התצוגות העסקיות בהתאמה אישית.
במקום להוסיף תצוגות בהתאמה אישית למשבצות של פריסות מוגדרות מראש, מטמיעים נציג פריסה כדי לנהל את הפריסה של המסך כולו. במהלך מעברי מצב בניווט, הנציג של הפריסה מקבל את הרכיבים ש-Google מספקת, כמו כרטיס הפנייה, כרטיסיית פרטי זמן הגעה משוער והלחצנים. אפשר למקם את הרכיבים האלה לצד התוכן המותאם אישית שלכם באמצעות אילוצים רגילים של Auto Layout ב-iOS.
אתם יכולים להשתמש במסגרת הזו כדי להציג מידע עסקי מותאם אישית, כמו סטטוס הזמנה או הוראות איסוף, במיקום מדויק במסך, בלי שהתצוגות יחפפו.
איך פועלת התכונה "התאמה אישית של הפריסה"
התאמה אישית של הפריסה משתמשת בתבנית עיצוב של הקצאת הרשאות. במקום לשרטט או למקם באופן אוטומטי רכיבים של ממשק המשתמש במסך, ה-SDK מעביר אותם ישירות למחלקה בהתאמה אישית שאתם כותבים: נציג הפריסה.
כדי להתאים אישית את הפריסה, יוצרים מחלקה שתואמת לפרוטוקולNavigationLayoutDelegate ומעבירים מופע ל-method setLayoutDelegate(_:) של GMSMapView. בכל פעם שמצב הניווט משתנה – למשל, מעבר מהמפה הבסיסית להנחיה פעילה של מסלול מפורט – ה-SDK מפעיל את שיטות הקריאה החוזרת (callback) בנציג ומספק את רכיבי ממשק המשתמש שזמינים למצב הספציפי הזה.
כדי לספק חוויית משתמש חלקה ומשולבת, המסגרת הזו יוצרת הפרדה ברורה בין תחומי האחריות של האפליקציה לבין אלה של ה-SDK. אפשר להשתמש ב-layout delegate כדי להתאים אישית את הדברים הבאים:
- בניית היררכיית התצוגות: בוחרים בדיוק אילו רכיבים של Google ותצוגות עסקיות בהתאמה אישית להוסיף למסך בכל מצב של ממשק המשתמש.
- מיקום של כל רכיב: הגדרת עוגנים מדויקים במסך, שוליים ומיקום הפריסה. אל תגדירו אילוצים מותאמים אישית לרוחב או לגובה של רכיבי Google, כי הרכיבים האלה מחשבים את המידות הפנימיות שלהם בעצמם.
- מסגור המפה הבסיסית: מגדירים את הגבולות הגלויים של מצלמת המפה באמצעות הקואורדינטות של רכיב אזור התצוגה.
- שכבות במסך: אתם יכולים להחליט אם התצוגות בהתאמה אישית יופיעו מעל, מתחת או לצד אמצעי הבקרה המובנים של Google.
בינתיים, אי אפשר להתאים אישית את הרכיבים הבאים של Google באמצעות נציג הפריסה:
- מידות הרכיבים: הגודל והמידות הפנימיות של הרכיבים ש-Google מספקת, שה-SDK מחשב באופן אוטומטי.
- תנאים להפעלת התראות: מתי מופיעות התראות או הנחיות דינמיות על סמך נתוני מסלול בזמן אמת.
עקרונות ההטמעה
כשכותבים נציג פריסה, חשוב לזכור את הכללים הבאים כדי למנוע באגים בפריסה או קריסות בזמן הריצה:
- כברירת מחדל, המסך ריק: הרכיבים ש-Google מספקת מופיעים רק אם הנציג מוסיף אותם באופן מפורש להיררכיית התצוגות וממקם אותם.
- אין תמיכה בממשקי API מדור קודם: מאפיינים ושיטות ששולטים במודל הפריסה מדור קודם שמבוסס על מיקומי מודעות לא ייתמכו, ויכול להיות שהם לא יפעלו כמו שצריך כשנציג בהתאמה אישית פעיל.
- אל תשנו את המבנים הפנימיים של התצוגות: אל תעברו על היררכיות התצוגות של רכיבים ש-Google מספקת (כמו כרטיס הפנייה או כרטיסיית פרטי זמן הגעה משוער) ואל תשנו אותן. היררכיות התצוגות הפנימיות האלה הן פרטי הטמעה בסיסיים, ולכן הן עשויות להשתנות בין גרסאות SDK. שינוי שלהם עלול לגרום לפריסה להשתבש בעדכונים עתידיים של ה-SDK.
תאימות ל-API מדור קודם
כדי שהפריסה תפעל בצורה אמינה כשמשתמשים בנציג פריסה בהתאמה אישית, כדאי להימנע משימוש בממשקי ה-API הקודמים הבאים שמבוססים על מיקומים, כי אנחנו מתכננים להוציא אותם משימוש. כדי להעביר אפליקציה קיימת, צריך להחליף את השימוש בממשקי ה-API האלה בקוד בנציג הפריסה בהתאמה אישית:
הצגת ממשקי API קודמים שמבוססים על מיקומי מודעות
| API מדור קודם | החלפת נציג הפריסה |
|---|---|
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(_:) |
להסתמך על פונקציות ה-callback של ההנחיות ב-NavigationLayoutDelegate,
כמו showPrompt(navMap:newPrompt:). |
כאשר מוגדר נציג פריסה בהתאמה אישית, למדריכי הפריסה הקודמים (כמו navigationHeaderLayoutGuide או bottomTrailingButtonsLayoutGuide) ולשיטות של תצוגת האביזרים בכותרת אין השפעה. במקום זאת, עגנו את התצוגות שלכם לתצוגות ש-Google מספקת.
המאפיין compassButton של GMSUISettings עדיין שולט במצפן כשממשק המשתמש של הניווט מושבת. כשהניווט מופעל, ה-delegate קובע אם להציג את המצפן מהמערך buttons.
רשימת משימות חיוניות
כדי להטמיע את נציג הפריסה בהצלחה, צריך לפעול לפי השלבים והדרישות הבאים:
-
מגדירים את ה-delegate לפני שמפעילים את הניווט: מפעילים את
setLayoutDelegate(_:)ב-GMSMapViewלפני שמפעילים את הניווט באמצעותisNavigationEnabledאוenableNavigation(with:). השיטה מחזירה שגיאה אם הניווט כבר מופעל או אם יצרתם את תצוגת המפה למסך CarPlay. תצוגת המפה שומרת הפניה חזקה ל-delegate. כדי לשחזר את הפריסה המובנית, מעבירים את הערך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) למאגר הפריסה בכל מצבי הניווט. -
Respect fixed dimensions: ה-SDK קובע את המידות של כמה רכיבים ש-Google מספקת. אל תגדירו אילוצים מותאמים אישית לרוחב או לגובה של הרכיבים הבאים:
- כרטיס פנייה
- כרטיסיית פרטי זמן הגעה משוער
- הנחיות נכנסות
- הלוגו של Google
- ווידג'ט מהירות
-
הוספת רכיבים כתצוגות משנה: מוסיפים את התצוגות ש-Google מספקת ישירות כתצוגות משנה לתצוגת הקונטיינר בהתאמה אישית או ל-
GMSMapViewעצמו. -
יישור באמצעות קווי עזר לפריסה: התצוגות ש-Google מספקת כוללות שוליים חזותיים מובנים. כדי ליישר את הרכיבים בהתאמה אישית בצורה מושלמת עם הרכיבים ש-Google מספקת, צריך להגביל את התצוגות למאפיין
layoutMarginsGuideשל התצוגה של Google. -
ניקוי ביציאה מהמצב: ב-
stopGuidance(navMap:newState:oldState:), מסירים את תצוגות ההנחיה הפעילות ומשביתים את האילוצים שלהן לפני שמשחזרים את הפריסה שמוכנה לניווט. ב-disableNavigationUI(navMap:oldState:), מסירים את כל התצוגות והמדריכים לפריסה שהוספתם, כולל כל הנחיה במסך. כך נמנעת חפיפה בין התצוגות של המצבים השונים. -
תמיכה בנגישות ובכתיבה מימין לשמאל: התצוגות ש-Google מספקת עומדות בתקני הנגישות, אבל הנציג שלכם אחראי לנגישות של הפריסה הכוללת. כדי לתמוך בשפות שנכתבות מימין לשמאל, צריך להשתמש בעוגנים בתחילת הטקסט ובסוף הטקסט במקום בעוגנים בצד שמאל ובצד ימין.
-
הימנעו משימוש בממשקי API מדור קודם לפריסה: אל תשתמשו במאפיינים מדור קודם לשינוי הפריסה ב-
GMSUISettingsאו במדריכי פריסה מדור קודם ב-GMSMapViewבזמן שהנציג בהתאמה אישית פעיל. יכול להיות שערכת ה-SDK תתעלם משיחות בשיטה הקודמת של Meet כשמצורף נציג בהתאמה אישית.
מצבים של ממשק המשתמש ורכיבים של Google
כשמתרחש מעבר בין מצבי ניווט, ה-SDK מעביר אובייקט NavigationUIState לקריאה בלבד לקריאה החוזרת (callback) של הנציג. האובייקט הזה מקבץ את דגלי הגדרת הפריסה הנוכחיים עם רכיבי Google (לדוגמה, מופעי UIView) שצריך כדי להציג את המסך.
ה-layout delegate מנהל את היררכיית התצוגות בארבעה מצבי פעולה. בתרשים המצבים הבא אפשר לראות איך ה-SDK עובר בין מצבי הניווט ואילו שיטות קריאה חוזרת (callback) הוא מפעיל ב-delegate:
רכיבי Google הספציפיים שמועברים לנציג תלויים בשלב הנוכחי במחזור החיים של הניווט.
רכיבי חובה (בכל מצבי הניווט)
לא משנה מהו שלב הניווט הנוכחי, צריך לכלול את הרכיבים הבאים של Google, למקם אותם ולשמור אותם גלויים בהיררכיית התצוגות:
הלוגו של Google (
googleLogo): הרכיב הזה מציג את הלוגו של מפות Google, שחובה להציג. אם הלחצן מרכוז מחדש מופעל, הלוגו הופך אוטומטית ללחצן הזה כשהנהג גולל את המפה הרחק מהרכב. לכן, המיקום של הלוגו קובע איפה יופיע הכפתור מרכוז מחדש. מומלץ למקם את הלוגו בפינה השמאלית התחתונה של הפריסה, כדי להתאים לציפיות של ממשק המשתמש הרגיל של המפה.אזור תצוגה (
viewport): רכיבUIViewבלתי נראה שמגדיר את גבולות הפריים של המצלמה. המיקום של אזור התצוגה קובע בדיוק איפה ה-SDK ממקם את סמל הרכב ומשרטט את קו המסלול הפעיל. ממקמים את אזור התצוגה כך שהוא יכסה את האזור הפתוח והלא מוסתר של המסך, כדי להימנע משכבות אטומות כמו גיליונות תחתונים בהתאמה אישית.
רכיבים שמוכנים לניווט
במצב "מוכן לניווט", ממשק המשתמש נשאר מינימלי כדי שהמיקוד יישאר במפה הבסיסית. האובייקט NavigationReadyUIState מספק גישה לפריטים הבאים:
- buttons: מערך של תצוגות פעולה צפות שמוגדרות למפה הבסיסית (בדרך כלל רק הכפתור "מצפן"). יכול להיות ש-Google תוסיף כפתורים או תשנה את הסדר שלהם בגרסאות עתידיות של ה-SDK, לכן אל תסתמכו על אינדקסים קבועים של מערכים. במקום זאת, צריך לעבור על המערך, לקרוא את הסוג של כל לחצן על ידי השוואה של המאפיין
typeשלו ל-NavigationButtonKnownType.compass, ולחלץ את התצוגה הפיזית על ידי קריאה למאפייןviewשלו. שימו לב שהכפתורים המדויקים במערך הזה שונים מאלה שזמינים במהלך הנחיה פעילה.
רכיבי הדרכה פעילים
כשההנחיות המפורטות מתחילות, ה-SDK פותח את כל חבילת אמצעי הבקרה לניווט. האובייקט ActiveGuidanceUIState מספק גישה לרכיבים הבאים של Google:
turnCard: הבאנר הראשי של הכותרת שבו מוצגים הוראות לתמרון הבא, מדידות מרחק והנחיות לנתיב. כדאי למקם אותו בחלק העליון של הפריסה כדי ליצור היררכיית ניווט מוכרת ולעגן את התצוגות המותאמות אישית סביבו.
etaCard: הבאנר בכותרת התחתונה שבו מוצגים זמן ההגעה המשוער, הזמן שנותר לנסיעה והמרחק שנותר עד ליעד. אפשר למקם אותו לאורך הקצה התחתון של המסך, או לשלב את הקואורדינטות שלו בגיליונות מותאמים אישית לניהול משימות.
tripProgressBar: סרגל התקדמות אנכי שמראה לנהג כמה הוא התקדם במסלול הנוכחי. בניגוד לפריסות הקודמות, שבהן הלחצן היה מקובע לקצה המפה, עכשיו אתם יכולים לעגן אותו בכל מקום שתרצו – למשל, לאורך הקצה הצדדי של מאגר התגים המותאם אישית.
speedWidget: פקד צף למד המהירות ולמהירות המותרת. בהתאם להגדרות ה-API (המאפייניםshouldDisplaySpeedometerו-shouldDisplaySpeedLimitשלGMSMapView) ולזמינות הנתונים, התצוגה הזו משנה את הגודל באופן דינמי בין ארבעה מצבים חזותיים בזמן הריצה (לא מוצג כלום, מוצגת רק המהירות הנוכחית, מוצגת רק מגבלת המהירות או מוצגות שתי הקריאות). מכיוון שהווידג'ט יכול לעבור בין הגדלים האלה בלי הודעה מוקדמת, תמיד צריך לעגן את התצוגות שמסביב באמצעות אילוצים כדי שהפריסה תתאים את עצמה באופן אוטומטי ותמנע חפיפות מרחביות.
-
buttons: מערך מורחב של תצוגות פעולה צפות למצב ההנחיה הפעיל (בדרך כלל כולל גם את המצפן וגם את הכפתור לדיווח על אירוע). בדיוק כמו במצב "מוכן לניווט", אתם יכולים לאתר ולחלץ תצוגות של לחצנים ספציפיים על ידי סינון המערך לפיtype(.compassאו.reporting) וחילוץ התצוגה הפיזית באמצעות המאפייןviewשלה. אחר כך אפשר למקם אותם בנפרד או להוסיף אותם ל-GMSNavigationHidingStackView. תצוגת המקבץ הזו מסדרת את הלחצנים בצורה אנכית, וכשנגמר המקום, מסתירה אותם החל מהסוף של המערךarrangedSubviews. אל תגדירו אתisHiddenבתצוגות שהוא מנהל.
NavigationStyleValues.headerNominalHeight, והוא מספיק גבוה לכל תוכן של כרטיס פנייה. כדי לשמור על יציבות של מסגור המצלמה, מגבילים את החלק העליון של viewport ישירות לקצה התחתון של turnCard, כמו שמוצג בהטמעה לדוגמה.
רכיבים של הנחיות דינמיות
הנחיות, כמו התראות על אירועים או אזהרות על מצלמות מהירות, מופעלות באופן עצמאי במהלך ההנחיה הפעילה.
כשההודעה מוכנה להצגה, ערכת ה-SDK מפעילה את פונקציית ה-callback showPrompt(navMap:newPrompt:) של הנציג ומעבירה תצוגת newPrompt ישירות. הנציג אחראי על מיקום ההודעה הזו בצורה חלקה מעל הפריסה (בדרך כלל היא מעוגנת לקצה התחתון של מאגר המפות).
ההודעה שמופיעה מכסה את החלק התחתון של המסך, לכן צריך לעדכן את הפריסה כדי שההודעה לא תסתיר את אזור התצוגה, את הלוגו של Google או לחצנים שמיושרים לתחתית המסך.
הנציג גם שולט באופן שבו ההנחיות נכנסות למסך ויוצאות ממנו. למידע נוסף, קראו את המאמר התאמה אישית של הנחיות.
טיפול בגודל המסך ובמצב רחב
כדי לטפל בשינויים בממדי המפה ובכיוון המסך, נציג הפריסה משתמש בתכונות הבאות:
שינוי גודל התצוגה: התאמת הפריסה בכל פעם שהגודל הפיזי של המפה משתנה.
מצב רחב: מעבר לפריסה בפורמט רחב כשהמפה רחבה מספיק.
תגובה לשינוי גודל התצוגה
פריסות של מסך מפוצל, פסי גלילה של קונטיינרים וסיבוב המכשיר בדרך כלל מפעילים את הקריאה החוזרת (callback) שלsizeChanged(navMap:newState:). אפשר להשתמש בקריאה החוזרת הזו כדי לבצע התאמות רספונסיביות כלליות לממשק המשתמש המותאם אישית. כדי לשנות את המיקום של רכיבים בהתאמה אישית ליחס גובה-רוחב חדש של המסך, צריך להטמיע את sizeChanged(navMap:newState:), להחיל נקודות עצירה מותאמות אישית של רוחב או גובה, ולזהות מתי אירוע של שינוי גודל מחליף למצב רחב כדי להחליף בבטחה בין וריאציות של פריסה.
הסבר על המצב הרחב
מצב רחב מופעל כשמאגר המפה רחב מספיק כדי להציג רכיבי ממשק משתמש זה לצד זה.
קוראים את הערך הבוליאני wideMode מאובייקט המצב כדי לשנות את המיקום של רכיבי ממשק המשתמש המותאמים אישית, וכדי להשאיר את מרכז המפה פנוי לנהג. מכיוון שרכיבים ש-Google מספקת, כמו כרטיס הפנייה וכרטיסיית פרטי זמן הגעה משוער, מתכווצים ומשנים את צורתם באופן אוטומטי במצב רחב, קריאת הערך הבוליאני הזה מבטיחה שהפריסה תותאם בדיוק באותו רגע שבו הרכיבים של Google מתעדכנים.
הנה כמה דוגמאות למיקום של רכיבי הפריסה במצב רגיל ובמצב רחב:
מצב לאורך רגיל: כרטיס הפנייה ממוקם בחלק העליון של המסך וכרטיס זמן ההגעה המשוער ממוקם בחלק התחתון.
מצב רחב: כרטיסיית הפנייה וכרטיסיית פרטי זמן הגעה משוער מוצגות לאורך הקצה המוביל של המסך, כשכרטיסיית הפנייה נמצאת בחלק העליון וכרטיסיית פרטי זמן הגעה משוער נמצאת בחלק התחתון. אם תמקמו את אחד מהכרטיסים רחוק מהקצה המוביל, התוצאה תהיה התנהגות לא מוגדרת. אתם יכולים למקם את אזור התצוגה ואת התוכן שלכם בכל מקום. בדרך כלל ממקמים אותם בצד האחורי של הכרטיסים.
בדיקת המצב הרחב במהלך מעברי מצב
אם הפריסה תומכת בווריאציה של מצב רחב, צריך להעריך את הערך הבוליאני wideMode בתוך כל קריאה חוזרת (callback) של מעבר בין מצבים, במקום להסתמך רק על sizeChanged(navMap:newState:).
enableNavigationUI(navMap:newState:) ו-startGuidance(navMap:newState:oldState:) כדי לטפל בהפעלות ראשוניות של האפליקציה בכיוון לרוחב. כך אפשר לוודא שהפריסה בודקת את מצב המסך הרחב ומפעילה את גרסת הפריסה הנכונה (הרגילה או הרחבה) ברגע שמתחיל מצב ניווט חדש.
התאמה לסגנון של Google
כדי שהתצוגות בהתאמה אישית יתאימו לריווח של הרכיבים ש-Google מספקת, ה-SDK מספק את המחלקה NavigationStyleValues. מאפייני הכיתה שלו מחזירים מידות בנקודות.
לדוגמה, כדי להציב כפתור בהתאמה אישית בפינה העליונה של המסך מול כרטיס הפנייה, משתמשים ב-NavigationStyleValues.headerTopPadding בתור השוליים העליונים של הכפתור. הכפתור יתיישר עם הקצה העליון של כרטיס הפנייה.
אלה המדדים הזמינים:
NavigationStyleValues.buttonHorizontalPaddingNavigationStyleValues.buttonVerticalPaddingNavigationStyleValues.defaultCornerRadiusNavigationStyleValues.elementHorizontalPaddingNavigationStyleValues.headerFooterHorizontalPaddingNavigationStyleValues.headerNominalHeightNavigationStyleValues.headerTopPadding
התצוגות ש-Google מספקת כוללות גם שוליים מובנים. כשמציבים שתי תצוגות של Google זו על גבי זו, צריך להגביל אחת מהן ל-layoutMarginsGuide של השנייה כדי שהשוליים שלהן לא יצטברו. לדוגמה, כדי להציב את הווידג'ט של המהירות מעל כרטיסיית פרטי זמן ההגעה המשוער, מגבילים את העוגן התחתון של speedWidget ל-etaCard.layoutMarginsGuide.topAnchor.
התאמה אישית של הנחיות
ה-layout delegate קובע איך ההנחיות נכנסות למסך ויוצאות ממנו, ואילו מהפינות שלהן מעוגלות.
הנפשת הנחיות
ה-SDK לא מנפיש את ההנחיות בשבילכם. הנציג שלכם מחליט איך כל הנחיה מופיעה ונעלמת:
- ב-
showPrompt(navMap:newPrompt:), מוסיפים את ההנחיה לתצוגת המפה במיקום התחלתי, ואז מנפישים אותה למקום. לדוגמה, אפשר להתחיל את ההנחיה ממש מתחת לאזור הגלוי, או מאחורי גיליון תחתון כשהקצה העליון שלו מיושר עם החלק העליון של הגיליון. - ב-
hidePrompt(navMap:oldPrompt:), מוסיפים אנימציה להודעה כדי להוציא אותה מהתצוגה, ואז מסירים אותה מהתצוגה הראשית כשהאנימציה מסתיימת. - ב-
changePrompt(navMap:newPrompt:oldPrompt:), מפעילים את האנימציות של ההסתרה וההצגה בו-זמנית כדי להחליף הנחיה אחת באחרת.
ההטמעה לדוגמה משתמשת בגישה הזו.
כדי להשבית את ההנחיות לגבי תנועה, מסלולים טובים יותר ואירועים, מגדירים את המאפיין shouldDisplayPrompts של GMSNavigator לערך false.
עיגול הפינות של ההנחיה
ההנחיות תואמות לפרוטוקול NavigationRoundedCornerView, כך שאפשר לבחור אילו פינות יהיו מעוגלות. כברירת מחדל, רק הפינות העליונות של ההודעה מעוגלות, וזה מתאים להודעה שמעוגנת לקצה התחתון של המסך.
אם מציבים הנחיה מעל תוכן אחר, כמו גיליון תחתון, מעגלים את כל ארבע הפינות:
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)
}
}
כדי להשתמש ב-delegate, צריך להגדיר אותו בתצוגת המפה לפני שמפעילים את הניווט, כמו שמתואר ברשימת המשימות החיוניות.
מגבלות ובעיות ידועות
התאמה אישית של הפריסה לא חלה על CarPlay, שמשתמש בתבניות פריסה קבועות. אם תפעילו את setLayoutDelegate(_:) בתצוגת מפה שתיצרו למסך CarPlay, השיטה תציג שגיאה.
לבעיות שמשפיעות על גרסאות SDK ספציפיות, עיינו בקטע בעיות מוכרות בהערות הגרסה.