איך יוצרים את המפה התלת-ממדית הראשונה באמצעות SwiftUI

1. לפני שמתחילים

בשיעור הזה תלמדו איך ליצור אפליקציית מפות תלת-ממד ב-SwiftUI באמצעות Maps 3D SDK ל-iOS.

אפליקציה שמוצגת בה מפה תלת-ממדית של סן פרנסיסקו

תלמדו:

  • איך שולטים במצלמה כדי להציג מיקומים ולעוף מעל המפה.
  • איך מוסיפים סמנים ומודלים
  • איך שרטטים קווים ופוליגונים
  • איך מטפלים בקליקים של משתמשים על סמנים של מקומות.

דרישות מוקדמות

  • פרויקט במסוף Google שבו החיוב מופעל
  • מפתח API, שאפשר להגביל אותו ל-Maps 3D SDK ל-iOS.
  • ידע בסיסי בפיתוח iOS באמצעות SwiftUI.

הפעולות שתבצעו:

  • הגדרת Xcode והוספת ה-SDK באמצעות Swift Package Manager
  • הגדרת האפליקציה לשימוש במפתח API
  • הוספת מפה תלת-ממדית בסיסית לאפליקציה
  • לשלוט במצלמה כדי לטוס למיקומים ספציפיים ולעוף סביבם
  • הוספת סמנים, קווים, פוליגונים ומודלים למפה

מה נדרש

  • Xcode מגרסה 15 ואילך.

2. להגדרה

בשלב ההפעלה הבא, תצטרכו להפעיל את Maps 3D SDK ל-iOS.

הגדרת הפלטפורמה של מפות Google

אם עדיין אין לכם חשבון ב-Google Cloud Platform ופרויקט שבו החיוב מופעל, תוכלו להיעזר במדריך תחילת העבודה עם פלטפורמת מפות Google כדי ליצור חשבון לחיוב ופרויקט.

  1. ב-Cloud Console, לוחצים על התפריט הנפתח של הפרויקט ובוחרים את הפרויקט שבו רוצים להשתמש ב-codelab הזה.

  1. מפעילים את ממשקי ה-API וערכות ה-SDK של הפלטפורמה של מפות Google הנדרשים לסדנת הקוד הזו ב-Google Cloud Marketplace. כדי לעשות זאת, פועלים לפי השלבים שמפורטים בסרטון הזה או במסמך הזה.
  2. יוצרים מפתח API בדף Credentials במסוף Cloud. אפשר לפעול לפי השלבים שמפורטים בסרטון הזה או במסמך הזה. כל הבקשות לפלטפורמה של מפות Google מחייבות מפתח API.

הפעלת Maps 3D SDK ל-iOS

אפשר למצוא את Maps 3D SDK ל-iOS באמצעות הקישור בתפריט Google Maps Platform > APIs and Services במסוף.

לוחצים על Enable כדי להפעיל את ה-API בפרויקט שנבחר.

הפעלת Maps 3D SDK במסוף Google

3. יצירת אפליקציה בסיסית ב-SwiftUI

הערה: אפשר למצוא את קוד הפתרון לכל שלב במאגר של אפליקציית הדוגמה ב-codelab ב-GitHub .

יוצרים אפליקציה חדשה ב-Xcode.

הקוד של השלב הזה נמצא בתיקייה GoogleMaps3DDemo ב-GitHub.

פותחים את Xcode ויוצרים אפליקציה חדשה. מציינים את SwiftUI.

קוראים לאפליקציה GoogleMaps3DDemo, עם שם חבילה com.example.GoogleMaps3DDemo.

מייבאים את ספריית GoogleMaps3D לפרויקט

מוסיפים את ה-SDK לפרויקט באמצעות Swift Package Manager.

בפרויקט או בסביבת העבודה ב-Xcode, עוברים אל File (קובץ) > Add Package Dependencies (הוספת יחסי תלות בחבילות). מזינים את כתובת ה-URL https://github.com/googlemaps/ios-maps-3d-sdk, מקישים על Enter כדי לשלוח את החבילה ולוחצים על 'הוספת חבילה'.

בחלון Choose Package Products (בחירת מוצרים לחבילה), מוודאים ש-GoogleMaps3D יתווסף ליעד הראשי שהגדרתם. בסיום, לוחצים על 'הוספת חבילה'.

כדי לאמת את ההתקנה, עוברים לחלונית 'כללי' של היעד. בתפריט Frameworks, Libraries and Embedded Content (מסגרות, ספריות ותוכן מוטמע) אמורים להופיע החבילות שהותקנו. אפשר גם לעיין בקטע Package Dependencies (יחסי תלות בחבילות) ב-Project Navigator כדי לאמת את החבילה ואת הגרסה שלה.

הוספת מפתח ה-API

אפשר להטמיע את מפתח ה-API באפליקציה, אבל זו לא שיטה מומלצת. הוספת קובץ תצורה מאפשרת לשמור את מפתח ה-API בסוד, ומונעת את הצורך לבצע עליו בדיקת קוד (check-in) במערכת בקרת הגרסאות.

יצירת קובץ תצורה חדש בתיקיית השורש של הפרויקט

ב-Xcode, מוודאים שמוצג חלון ה-Project Explorer. לוחצים לחיצה ימנית על שורש הפרויקט ובוחרים באפשרות 'קובץ חדש מתבנית'. גוללים עד שמגיעים לקטע 'Configuration Settings File'. בוחרים באפשרות הזו ולוחצים על 'הבא'. נותנים לקובץ את השם Config.xcconfig ומוודאים שבחרתם בתיקיית השורש של הפרויקט. לוחצים על 'יצירה' כדי ליצור את הקובץ.

בעורך, מוסיפים שורה לקובץ התצורה באופן הבא: MAPS_API_KEY = YOUR_API_KEY

מחליפים את הערך YOUR_API_KEY במפתח ה-API שלכם.

מוסיפים את ההגדרה הזו ל-Info.plist.

כדי לעשות זאת, בוחרים את שורש הפרויקט ולוחצים על הכרטיסייה 'מידע'.

מוסיפים נכס חדש בשם MAPS_API_KEY עם הערך $(MAPS_API_KEY).

בקוד האפליקציה לדוגמה יש קובץ Info.plist שמציין את המאפיין הזה.

הוספת מפה

פותחים את הקובץ GoogleMaps3DDemoApp.swift. זוהי נקודת הכניסה והניווט הראשי של האפליקציה.

הוא קורא ל-ContentView(), שמציג את ההודעה Hello World.

פותחים את ContentView.swift בעורך.

מוסיפים הצהרת import עבור GoogleMaps3D.

מוחקים את הקוד בתוך בלוק הקוד var body: some View {}. מגדירים Map() חדש בתוך body.

ההגדרה המינימלית שצריך כדי לאתחל Map היא MapMode. יש לשדה הזה שני ערכים אפשריים:

  • .hybrid – תמונות לוויין עם כבישים ותיאורי מקום, או
  • .satellite – תמונות לוויין בלבד.

הצוות של .hybrid ישמח לתת לכם שירות.

קובץ ContentView.swift אמור להיראות כך.

import GoogleMaps3D
import SwiftUI

@main
struct ContentView: View {
    var body: some View {
      Map(mode: .hybrid)
    }
}

מגדירים את מפתח ה-API.

צריך להגדיר את מפתח ה-API לפני שמפעילים את המפה.

כדי לעשות זאת, מגדירים את Map.apiKey בבורר האירועים init() של כל View שמכיל מפה. אפשר גם להגדיר אותו ב-GoogleMaps3DDemoApp.swift לפני שהוא קורא ל-ContentView().

ב-GoogleMaps3DDemoApp.swift, מגדירים את Map.apiKey בגורם המטפל באירועים onAppear של ה-WindowGroup.

אחזור מפתח ה-API מקובץ התצורה

משתמשים ב-Bundle.main.infoDictionary כדי לגשת להגדרה MAPS_API_KEY שיצרתם בקובץ התצורה.

import GoogleMaps3D
import SwiftUI

@main
struct GoogleMaps3DDemoApp: App {
  var body: some Scene {
    WindowGroup {
      ContentView()
    }
    .onAppear {
      guard let infoDictionary: [String: Any] = Bundle.main.infoDictionary else {
        fatalError("Info.plist not found")
      }
      guard let apiKey: String = infoDictionary["MAPS_API_KEY"] as? String else {
        fatalError("MAPS_API_KEY not set in Info.plist")
      }
      Map.apiKey = apiKey
    }
  }
}

מפתחים ומפעילים את האפליקציה כדי לבדוק שהיא נטענת כמו שצריך. אמורה להופיע מפה של העולם.

מפה תלת-ממדית של כדור הארץ

4. שימוש במצלמה כדי לשלוט בתצוגת המפה

יצירת אובייקט של מצב המצלמה

תצוגות המפה בתלת-ממד נשלטות על ידי הכיתה Camera. בשלב הזה תלמדו לציין את המיקום, הגובה, כיוון המצפן, הטיה, תנועת רוטציה והטווח כדי להתאים אישית את תצוגת המפה.

תצוגת מפה תלת-ממדית של סן פרנסיסקו

יצירת כיתה Helpers לאחסון הגדרות המצלמה

מוסיפים קובץ ריק חדש בשם MapHelpers.swift. בקובץ החדש, מייבאים את GoogleMaps3D ומוסיפים תוסף לכיתה Camera. מוסיפים משתנה בשם sanFrancisco. מאתחלים את המשתנה הזה כאובייקט Camera חדש. ממקמים את המצלמה ב-latitude: 37.39, longitude: -122.08.

import GoogleMaps3D

extension Camera {
 public static var sanFrancisco: Camera = .init(latitude: 37.39, longitude: -122.08)
}

הוספת תצוגה מפורטת חדשה לאפליקציה

יוצרים קובץ חדש בשם CameraDemo.swift. מוסיפים לקובץ את המתאר הבסיסי של תצוגה חדשה של SwiftUI.

מוסיפים משתנה @State בשם camera מסוג Camera. מאתחלים אותו למצלמה sanFrancisco שהגדרתם.

השימוש ב-@State מאפשר לקשר את המפה למצב המצלמה ולהשתמש בה כמקור האמיתי.

@State var camera: Camera = .sanFrancisco

משנים את קריאת הפונקציה Map() כך שתכלול את המאפיין camera. משתמשים בקישור המצב של המצלמה $camera כדי לאתחל את המאפיין camera באובייקט @State של המצלמה (.sanFrancisco).

import SwiftUI
import GoogleMaps3D

struct CameraDemo: View {
  @State var camera: Camera = .sanFrancisco
  var body: some View {
    VStack {
      Map(camera: $camera, mode: .hybrid)
    }
  }
}

הוספת ממשק משתמש בסיסי לניווט לאפליקציה

מוסיפים NavigationView לנקודת הכניסה הראשית של האפליקציה, GoogleMaps3DDemoApp.swift.

כך המשתמשים יוכלו לראות רשימה של הדגמות וללחוץ על כל אחת מהן כדי לפתוח אותה.

עורכים את GoogleMaps3DDemoApp.swift כדי להוסיף NavigationView חדש.

מוסיפים List שמכיל שתי הצהרות NavigationLink.

ה-NavigationLink הראשון אמור לפתוח את ContentView() עם תיאור Text Basic Map.

ה-NavigationLink השני אמור לפתוח את CameraDemo().

...
      NavigationView {
        List {
          NavigationLink(destination: ContentView()) {
            Text("Basic Map")
          }
          NavigationLink(destination: CameraDemo()) {
            Text("Camera Demo")
          }
        }
      }
...

הוספת תצוגה מקדימה של Xcode

תצוגות מקדימות הן תכונה חזקה של Xcode שמאפשרת לכם לראות את האפליקציה ולבצע בה פעולות בזמן שאתם מבצעים בה שינויים.

כדי להוסיף תצוגה מקדימה, פותחים את CameraDemo.swift. מוסיפים בלוק קוד של #Preview {} מחוץ ל-struct.

#Preview {
 CameraDemo()
}

פותחים או מרעננים את חלונית התצוגה המקדימה ב-Xcode. במפה אמורה להופיע סן פרנסיסקו.

הגדרה של תצוגות תלת-ממדיות בהתאמה אישית

אפשר לציין פרמטרים נוספים כדי לשלוט במצלמה:

  • heading: הכיוון במעלות מצפון שאליו מכוונים את המצלמה.
  • tilt: זווית ההטיה במעלות, כאשר 0 היא ישירות מעל הראש ו-90 היא צפייה אופקית.
  • roll: זווית הנטייה סביב המטוס האנכי של המצלמה, במעלות
  • range: המרחק במטרים של המצלמה מהמיקום לפי קו הרוחב וקו האורך
  • altitude: הגובה של המצלמה מעל פני הים

אם לא תספקו אף אחד מהפרמטרים הנוספים האלה, המערכת תשתמש בערכי ברירת המחדל.

כדי להציג יותר נתוני תלת-ממד בתצוגת המצלמה, מגדירים את הפרמטרים הראשוניים כך שיציגו תצוגה קרובה יותר ומשופעת.

עורכים את Camera שהגדרתם ב-MapHelpers.swift כך שיכלול ערכים עבור altitude, ‏ heading, ‏ tilt, ‏ roll ו-range

public static var sanFrancisco: Camera = .init(
  latitude: 37.7845812,
  longitude: -122.3660241,
  altitude: 585,
  heading: 288.0,
  tilt: 75.0,
  roll: 0.0,
  range: 100)

מפתחים ומפעילים את האפליקציה כדי לראות ולחקור את התצוגה החדשה בתלת-ממד.

5. אנימציות בסיסיות של מצלמה

עד עכשיו השתמשתם במצלמה כדי לציין מיקום יחיד עם הטיה, גובה, כיוון וטווח. בשלב הזה תלמדו איך להזיז את תצוגת המצלמה באמצעות אנימציה של המאפיינים האלה ממצב ראשוני למצב חדש.

מפה תלת-ממדית של סיאטל

מעבר בטיסה למיקום מסוים

משתמשים בשיטה Map.flyCameraTo() כדי ליצור אנימציה של המצלמה מהמיקום הראשוני למיקום חדש.

השיטה flyCameraTo() מקבלת מספר פרמטרים:

  • Camera שמייצג את מיקום הסיום.
  • duration: משך הזמן שבו האנימציה תפעל, בשניות.
  • trigger: אובייקט שניתן לצפות בו, שיפעיל את האנימציה כשהסטטוס שלו ישתנה.
  • completion: קוד שיתבצע בסיום האנימציה.

הגדרת מיקום לטיסה

פותחים את קובץ ה-MapHelpers.swift.

מגדירים אובייקט מצלמה חדש כדי להציג את סיאטל.

public static var seattle: Camera = .init(latitude:
47.6210296,longitude: -122.3496903, heading: 149.0, tilt: 77.0, roll: 0.0, range: 4000)

מוסיפים לחצן כדי להפעיל את ההנפשה.

פותחים את CameraDemo.swift. מגדירים משתנה בוליאני חדש בתוך struct.

נקרא לו animate עם ערך ראשוני של false.

@State private var animate: Bool = false

מוסיפים Button מתחת ל-VStack. ה-Button יפעיל את האנימציה של המפה.

נותנים ל-Button Text מתאים, למשל 'התחלת הטיסה'.

import SwiftUI
import GoogleMaps3D

struct CameraDemo: View {
  @State var camera:Camera = .sanFrancisco
  @State private var animate: Bool = false

  var body: some View {
    VStack{
      Map(camera: $camera, mode: .hybrid)
      Button("Start Flying") {
      }
    }
  }
}

ב-Button closure מוסיפים קוד כדי להחליף את המצב של המשתנה animate.

      Button("Start Flying") {
        animate.toggle()
      }

מפעילים את האנימציה.

מוסיפים את הקוד כדי להפעיל את האנימציה flyCameraTo() כשהמצב של המשתנה animate משתנה.

  var body: some View {
    VStack{
      Map(camera: $camera, mode: .hybrid)
        .flyCameraTo(
          .seattle,
          duration: 5,
          trigger: animate,
          completion: {  }
        )
      Button("Start Flying") {
        animate.toggle()
      }
    }
  }

לטוס סביב מיקום

אפשר להשתמש בשיטה Map.flyCameraAround() כדי לעוף סביב מיקום מסוים. לשיטה הזו יש כמה פרמטרים:

  • Camera שמגדיר את המיקום והתצוגה.
  • duration בשניות.
  • rounds: מספר הפעמים שרוצים לחזור על האנימציה.
  • trigger: אובייקט שניתן לצפות בו שיפעיל את האנימציה.
  • callback: קוד שיופעל כשהאנימציה תרוץ.

מגדירים משתנה @State חדש בשם flyAround, עם ערך ראשוני של false.

לאחר מכן, מוסיפים קריאה ל-flyCameraAround() מיד אחרי קריאת השיטה flyCameraTo().

משך הטיסה צריך להיות ארוך יחסית כדי שהתצוגה תשתנה בצורה חלקה.

חשוב להפעיל את האנימציה flyCameraAround() על ידי שינוי המצב של אובייקט הטריגר כשהפעולה flyCameraTo() מסתיימת.

הקוד אמור להיראות כך.

import SwiftUI
import GoogleMaps3D

struct CameraDemo: View {
  @State var camera:Camera = .sanFrancisco
  @State private var animate: Bool = false
  @State private var flyAround: Bool = false

  var body: some View {
    VStack{
      Map(camera: $camera, mode: .hybrid)
        .flyCameraTo(
          .seattle,
          duration: 5,
          trigger: animate,
          completion: { flyAround = true }
        )
        .flyCameraAround(
          .seattle,
          duration: 15,
          rounds: 0.5,
          trigger: flyAround,
          callback: {  }
        )
      Button("Start Flying") {
        animate.toggle()
      }
    }
  }
}

#Preview {
  CameraDemo()
}

בודקים את התצוגה המקדימה של האפליקציה או מפעילים אותה כדי לראות שהמצלמה טסה סביב היעד אחרי שהאנימציה flyCameraTo() מסתיימת.

6. מוסיפים סמן למפה.

בשלב הזה תלמדו איך לצייר סיכה במפה.

תיצורו אובייקט Marker ותוסיפו אותו למפה. ערכת ה-SDK תשתמש בסמל ברירת מחדל לסמן. בסיום, תשנו את הגובה של הסמן ומאפיינים אחרים כדי לשנות את אופן התצוגה שלו.

מפה תלת-ממדית עם סמן במפה

יוצרים תצוגה חדשה של SwiftUI לדמו של Marker.

מוסיפים קובץ Swift חדש לפרויקט. נקרא לה MarkerDemo.swift.

מוסיפים את המתאר של תצוגת SwiftUI ומפעילים את המפה כמו שעשיתם ב-CameraDemo.

import SwiftUI
import GoogleMaps3D

struct MarkerDemo: View {
  @State var camera: Camera = .sanFrancisco
  var body: some View {
    VStack {
      Map(camera: $camera, mode: .hybrid)
    }
  }
}

איך מפעילים אובייקט Marker

מגדירים משתנה סמן חדש בשם mapMarker. בחלק העליון של בלוק הקוד struct ב-MarkerDemo.swift.

מציבים את ההגדרה בשורה שמתחת להצהרה camera. קוד לדוגמה שמאפס את כל הנכסים הזמינים.

  @State var mapMarker: Marker = .init(
    position: .init(
      latitude: 37.8044862,
      longitude: -122.4301493,
      altitude: 0.0),
    altitudeMode: .absolute,
    collisionBehavior: .required,
    extruded: false,
    drawsWhenOccluded: true,
    sizePreserved: true,
    zIndex: 0,
    label: "Test"
  )

מוסיפים את הסמן למפה.

כדי לצייר את הסמן, מוסיפים אותו לחסימה שנקראת כשהמפה נוצרת.

struct MarkerDemo: View {
  @State var camera: Camera = .sanFrancisco
  var body: some View {
    VStack {
      Map(camera: $camera, mode: .hybrid) {
        mapMarker
      }
    }
  }
}

מוסיפים NavigationLink חדש ל-GoogleMaps3DDemoApp.swift עם יעד MarkerDemo() ו-Text ומתארים אותו בתור 'דוגמה לסמן'.

...
      NavigationView {
        List {
          NavigationLink(destination: Map()) {
            Text("Basic Map")
          }
          NavigationLink(destination: CameraDemo()) {
            Text("Camera Demo")
          }
          NavigationLink(destination: MarkerDemo()) {
            Text("Marker Demo")
          }
        }
      }
...

תצוגה מקדימה והפעלה של האפליקציה

מרעננים את התצוגה המקדימה או מריצים את האפליקציה כדי לראות את הסמן.

סמנים מורחבים

אפשר להציב סמנים מעל הקרקע או מעל רשת התלת-מימד באמצעות המקשים altitude ו-altitudeMode.

מעתיקים את ההצהרה על mapMarker ב-MarkerDemo.swift למשתנה Marker חדש שנקרא extrudedMarker.

מגדירים ערך שאינו אפס עבור altitude, 50 מספיק.

משנים את הערך של altitudeMode ל-.relativeToMesh ומגדירים את extruded ל-true. כדי למקם את הסמן בראש גורד שחקים, משתמשים ב-latitude וב-longitude מקטע הקוד הזה.

  @State var extrudedMarker: Marker = .init(
    position: .init(
      latitude: 37.78980534,
      longitude:  -122.3969349,
      altitude: 50.0),
    altitudeMode: .relativeToMesh,
    collisionBehavior: .required,
    extruded: true,
    drawsWhenOccluded: true,
    sizePreserved: true,
    zIndex: 0,
    label: "Extruded"
  )

מריצים או מציגים תצוגה מקדימה של האפליקציה שוב. הסמן אמור להופיע מעל בניין תלת-ממדי.

7. מוסיפים מודל למפה.

אפשר להוסיף Model באותו אופן שבו מוסיפים Marker. תצטרכו קובץ מודל שאפשר לגשת אליו באמצעות כתובת URL או להוסיף כקובץ מקומי בפרויקט. בשלב הזה נשתמש בקובץ מקומי שאפשר להוריד מהמאגר ב-GitHub של סדנת הקוד הזו.

מפה תלת-ממדית של סן פרנסיסקו עם דגם של כדור פורח

הוספת קובץ מודל לפרויקט

יוצרים תיקייה חדשה בשם Models בפרויקט Xcode.

מורידים את המודל ממאגר האפליקציות לדוגמה ב-GitHub. מוסיפים אותו לפרויקט על ידי גרירה לתיקייה החדשה בתצוגת הפרויקט ב-Xcode.

חשוב לוודא שהגדרתם את היעד הזה כיעד הראשי של האפליקציה.

בודקים את ההגדרות של Build Phases‏ > Copy Bundle Resources בפרויקט. קובץ המודל צריך להופיע ברשימת המשאבים שהועברו לחבילה. אם היא לא מופיעה, לוחצים על '+' כדי להוסיף אותה.

מוסיפים את המודל לאפליקציה.

יוצרים קובץ SwiftUI חדש בשם ModelDemo.swift.

מוסיפים הצהרות import עבור SwiftUI ו-GoogleMaps3D כמו בשלבים הקודמים.

מגדירים Map בתוך VStack ב-body.

import SwiftUI
import GoogleMaps3D

struct ModelDemo: View {
  @State var camera: Camera = .sanFrancisco
  
  var body: some View {
    VStack {
      Map(camera: $camera, mode: .hybrid) {
        
      }
    }
  }
}

מקבלים את נתיב המודל מה-Bundle. מוסיפים את הקוד הזה מחוץ ל-struct.

private let fileUrl = Bundle.main.url(forResource: "balloon", withExtension: "glb")

מגדירים משתנה למודל בתוך המבנה.

מציינים ערך ברירת מחדל למקרה שלא יצוין ערך ל-fileUrl.

  @State var balloonModel: Model = .init(
    position: .init(
      latitude: 37.791376,
      longitude: -122.397571,
      altitude: 300.0),
    url: URL(fileURLWithPath: fileUrl?.relativePath ?? ""),
    altitudeMode: .absolute,
    scale: .init(x: 5, y: 5, z: 5),
    orientation: .init(heading: 0, tilt: 0, roll: 0)
  )

3. משתמשים במודל במפה.

בדומה להוספת Marker, פשוט מספקים את ההפניה ל-Model בהצהרה Map.

  var body: some View {
    VStack {
      Map(camera: $camera, mode: .hybrid) {
        balloonModel
      }
    }
  }

תצוגה מקדימה והפעלה של האפליקציה

מוסיפים NavigationLink חדש ל-GoogleMaps3DDemoApp.swift, עם יעד ModelDemo() ו-Text 'דוגמה לדגם'.

...
          NavigationLink(destination: ModelDemo()) {
            Text("Model Demo")
          }
...

מרעננים את התצוגה המקדימה או מריצים את האפליקציה כדי לראות את המודל.

8. מציירים קו ופוליגון במפה.

בשלב הזה תלמדו איך להוסיף קווים וצורות של פוליגונים למפה תלת-ממדית.

כדי לפשט את התהליך, נגדיר את הצורות כמערכי אובייקטים מסוג LatLngAltitude. באפליקציה אמיתית, יכול להיות שהנתונים יוטמעו מקובץ, מקריאה ל-API או ממסד נתונים.

מפה תלת-ממדית של סן פרנסיסקו שמוצגים בה שני פוליגונים וקו פוליגוני

יוצרים כמה אובייקטים של צורות כדי לנהל את נתוני הצורות.

מוסיפים הגדרה חדשה של Camera ל-MapHelpers.swift שמתייחסת לדאונטאון של סן פרנסיסקו.

  public static var downtownSanFrancisco: Camera = .init(latitude: 37.7905, longitude: -122.3989, heading: 25, tilt: 71, range: 2500) 

מוסיפים קובץ חדש בשם ShapesDemo.swift לפרויקט. מוסיפים struct שנקרא ShapesDemo שמטמיע את פרוטוקול View, ומוסיפים לו body.

struct ShapesDemo: View {
  @State var camera: Camera = .downtownSanFrancisco

  var body: some View {
    VStack {
      Map(camera: $camera, mode: .hybrid) {

      }
    }
  }
}

הכיתות שבהן משתמשים לניהול נתוני הצורות הן Polyline ו-Polygon. פותחים את ShapesDemo.swift ומוסיפים אותם ל-struct באופן הבא.

var polyline: Polyline = .init(coordinates: [
    LatLngAltitude(latitude: 37.80515638571346, longitude: -122.4032569467164, altitude: 0),
    LatLngAltitude(latitude: 37.80337073509504, longitude: -122.4012878349353, altitude: 0),
    LatLngAltitude(latitude: 37.79925208843463, longitude: -122.3976697250461, altitude: 0),
    LatLngAltitude(latitude: 37.7989102378512, longitude: -122.3983408725656, altitude: 0),
    LatLngAltitude(latitude: 37.79887832784348, longitude: -122.3987094864192, altitude: 0),
    LatLngAltitude(latitude: 37.79786443410338, longitude: -122.4066878788802, altitude: 0),
    LatLngAltitude(latitude: 37.79549248916587, longitude: -122.4032992702785, altitude: 0),
    LatLngAltitude(latitude: 37.78861484290265, longitude: -122.4019489189814, altitude: 0),
    LatLngAltitude(latitude: 37.78618687561075, longitude: -122.398969592545, altitude: 0),
    LatLngAltitude(latitude: 37.7892310309145, longitude: -122.3951458683092, altitude: 0),
    LatLngAltitude(latitude: 37.7916358762409, longitude: -122.3981969390652, altitude: 0)
  ])
  .stroke(GoogleMaps3D.Polyline.StrokeStyle(
    strokeColor: UIColor(red: 0.09803921568627451, green: 0.403921568627451, blue: 0.8235294117647058, alpha: 1),
    strokeWidth: 10.0,
    outerColor: .white,
    outerWidth: 0.2
    ))
  .contour(GoogleMaps3D.Polyline.ContourStyle(isGeodesic: true))

  var originPolygon: Polygon = .init(outerCoordinates: [
    LatLngAltitude(latitude: 37.79165766856578, longitude:  -122.3983762901255, altitude: 300),
    LatLngAltitude(latitude: 37.7915324439261, longitude:  -122.3982171091383, altitude: 300),
    LatLngAltitude(latitude: 37.79166617650914, longitude:  -122.3980478493319, altitude: 300),
    LatLngAltitude(latitude: 37.79178986470217, longitude:  -122.3982041104199, altitude: 300),
    LatLngAltitude(latitude: 37.79165766856578, longitude:  -122.3983762901255, altitude: 300 )
  ],
  altitudeMode: .relativeToGround)
  .style(GoogleMaps3D.Polygon.StyleOptions(fillColor:.green, extruded: true) )

  var destinationPolygon: Polygon = .init(outerCoordinates: [
      LatLngAltitude(latitude: 37.80515661739527, longitude:  -122.4034307490334, altitude: 300),
      LatLngAltitude(latitude: 37.80503794515428, longitude:  -122.4032633416024, altitude: 300),
      LatLngAltitude(latitude: 37.80517850164195, longitude:  -122.4031056058006, altitude: 300),
      LatLngAltitude(latitude: 37.80529346901115, longitude:  -122.4032622466595, altitude: 300),
      LatLngAltitude(latitude: 37.80515661739527, longitude:  -122.4034307490334, altitude: 300 )
  ],
  altitudeMode: .relativeToGround)
  .style(GoogleMaps3D.Polygon.StyleOptions(fillColor:.red, extruded: true) )

שימו לב לפרמטרים של האיפוס שנעשה בהם שימוש.

  • altitudeMode: .relativeToGround משמש להוצאה (extrude) של הפוליגונים לגובה ספציפי מעל הקרקע.
  • השדה altitudeMode: .clampToGround משמש כדי לגרום לקו הפוליגון לפעול בהתאם לצורה של פני כדור הארץ.
  • הסגנונות מוגדרים באובייקטים Polygon על ידי קישור של קריאה ל-method styleOptions() אחרי הקריאה ל-.init()

הוספת הצורות למפה

בדומה לשלבים הקודמים, אפשר להוסיף את הצורות ישירות לחסימה Map. יוצרים את ה-Map בתוך VStack.

...
  var body: some View {
    VStack {
      Map(camera: $camera, mode: .hybrid) {
        polyline
        originPolygon
        destinationPolygon
      }
    }
  }
...

תצוגה מקדימה והפעלה של האפליקציה

מוסיפים קוד תצוגה מקדימה ובודקים את האפליקציה בחלונית התצוגה המקדימה ב-Xcode.

#Preview {
  ShapesDemo()
}

כדי להריץ את האפליקציה, מוסיפים NavigationLink חדש ל-GoogleMaps3DDemoApp.swift שפותח את תצוגת הדגמה החדשה.

...
          NavigationLink(destination: ShapesDemo()) {
            Text("Shapes Demo")
          }
...

מריצים את האפליקציה ומעיינים בצורות שהוספתם.

9. טיפול באירועי הקשה על סמנים של מקומות

בשלב הזה תלמדו איך להגיב להקשות של משתמשים על סמנים של מקומות.

מפה שמציגה חלון קופץ עם מזהה מקום

הערה: כדי לראות את סמלי המיקומים במפה, צריך להגדיר את MapMode כ-.hybrid.

כדי לטפל בהקשה, צריך להטמיע את השיטה Map.onPlaceTap.

האירוע onPlaceTap מספק אובייקט PlaceTapInfo שממנו אפשר לקבל את מזהה המקום של סמן המקום שנלחץ עליו.

אפשר להשתמש במזהה המקום כדי לחפש פרטים נוספים באמצעות Places SDK או Places API.

הוספת תצוגה מפורטת חדשה של Swift

מוסיפים את הקוד הבא לקובץ Swift חדש בשם PlaceTapDemo.swift.

import GoogleMaps3D
import SwiftUI

struct PlaceTapDemo: View {
  @State var camera: Camera = .sanFrancisco
  @State var isPresented = false
  @State var tapInfo: PlaceTapInfo?

  var body: some View {
    Map(camera: $camera, mode: .hybrid)
      .onPlaceTap { tapInfo in
        self.tapInfo = tapInfo
        isPresented.toggle()
      }
      .alert(
        "Place tapped - \(tapInfo?.placeId ?? "nil")",
        isPresented: $isPresented,
        actions: { Button("OK") {} }
      )
  }
}
#Preview {
  PlaceTapDemo()
}

תצוגה מקדימה והפעלה של האפליקציה

פותחים את חלונית התצוגה המקדימה כדי לראות תצוגה מקדימה של האפליקציה.

כדי להריץ את האפליקציה, מוסיפים NavigationLink חדש ל-GoogleMaps3DDemoApp.swift.

...
          NavigationLink(destination: PlaceTapDemo()) {
            Text("Place Tap Demo")
          }
...

10. (אופציונלי) המשך התהליך

אנימציות מתקדמות במצלמה

בתרחישי שימוש מסוימים נדרשת אנימציה חלקה לאורך רצף או רשימה של מיקומים או מצבי מצלמה, למשל בסימולטור טיסה או בהפעלה חוזרת של טיול או ריצה.

בשלב הזה תלמדו איך לטעון רשימה של מיקומים מקובץ, ולהציג אנימציה של כל מיקום ברצף.

תצוגת מפה תלת-ממדית של הגישה לאינסברוק

טעינת קובץ שמכיל רצף של מיקומים.

מורידים את flightpath.json ממאגר האפליקציות לדוגמה ב-GitHub.

יוצרים תיקייה חדשה בשם JSON בפרויקט Xcode.

גוררים את flightpath.json לתיקייה JSON ב-Xcode.

מגדירים את היעד הזה כיעד הראשי של האפליקציה. בודקים שהקובץ הזה נכלל בהגדרות של Copy Bundle Resources בפרויקט.

יוצרים באפליקציה שני קובצי Swift חדשים בשם FlightPathData.swift ו-FlightDataLoader.swift.

מעתיקים את הקוד הבא לאפליקציה. הקוד הזה יוצר מבנים וסוגי נתונים (classes) שקוראים קובץ מקומי שנקרא 'flighpath.json' ומפענחים אותו כ-JSON.

המבנים FlightPathData ו-FlightPathLocation מייצגים את מבנה הנתונים בקובץ ה-JSON כאובייקטים של Swift.

הכיתה FlightDataLoader קוראת את הנתונים מהקובץ ומפענחת אותם. הוא משתמש בפרוטוקול ObservableObject כדי לאפשר לאפליקציה לעקוב אחרי השינויים בנתונים שלה.

הנתונים המנותחים נחשפים דרך נכס שפורסם.

FlightPaths.swift

import GoogleMaps3D

struct FlightPathData: Decodable {
  let flight: [FlightPathLocation]
}

struct FlightPathLocation: Decodable {
  let timestamp: Int64
  let latitude: Double
  let longitude: Double
  let altitude: Double
  let bearing: Double
  let speed: Double
}

FlightDataLoader.swift

import Foundation

public class FlightDataLoader : ObservableObject {

  @Published var flightPathData: FlightPathData = FlightPathData(flight:[])
  @Published var isLoaded: Bool = false

  public init() {
    load("flightpath.json")
  }
  
  public func load(_ path: String) {
    if let url = Bundle.main.url(forResource: path, withExtension: nil){
      if let data = try? Data(contentsOf: url){
        let jsondecoder = JSONDecoder()
        do{
          let result = try jsondecoder.decode(FlightPathData.self, from: data)
          flightPathData = result
          isLoaded = true
        }
        catch {
          print("Error trying to load or parse the JSON file.")
        }
      }
    }
  }
}

הנפשת המצלמה לאורך כל מיקום

כדי להציג אנימציה של המצלמה בין רצף של שלבים, משתמשים ב-KeyframeAnimator.

כל Keyframe ייוצר כ-CubicKeyframe, כך שהשינויים במצב המצלמה יוצגו באנימציה חלקה.

שימוש ב-flyCameraTo() יגרום לתצוגה "להקפץ" בין כל מיקום.

קודם כול מגדירים מצלמה בשם 'innsbruck' ב-MapHelpers.swift.

public static var innsbruck: Camera = .init(
  latitude: 47.263,
  longitude: 11.3704,
  altitude: 640.08,
  heading: 237,
  tilt: 80.0,
  roll: 0.0,
  range: 200)

עכשיו מגדירים תצוגה חדשה בקובץ חדש בשם FlyAlongRoute.swift.

מייבאים את SwiftUI ואת GoogleMaps3D. מוסיפים Map ו-Button בתוך VStack. מגדירים את Button כדי להחליף את המצב של המשתנה הבוליאני animation.

מגדירים אובייקט State עבור FlightDataLoader, שיטען את קובץ ה-JSON כשהוא יופעל.

import GoogleMaps3D
import SwiftUI

struct FlyAlongRoute: View {
  @State private var camera: Camera = .innsbruck
  @State private var flyToDuration: TimeInterval = 5
  @State var animation: Bool = true

  @StateObject var flightData: FlightDataLoader = FlightDataLoader()

  var body: some View {
    VStack {
      Map(camera: $camera, mode: .hybrid)
      Button("Fly Along Route"){
        animation.toggle()
      }
    }
  }
}

יצירת הפריימים המרכזיים

התהליך הבסיסי הוא ליצור פונקציה שמחזירה פריים חדש בסדרת האנימציה. כל פריים חדש מגדיר את מצב המצלמה הבא שהאנימטור צריך ליצור עבורו אנימציה. אחרי שיוצרים את הפונקציה הזו, צריך להפעיל אותה עם כל מיקום מהקובץ ברצף.

מוסיפים שתי פונקציות למבנה FlyAlongRoute. הפונקציה makeKeyFrame מחזירה CubicKeyframe עם מצב מצלמה. הפונקציה makeCamera מקבלת שלב ברצף נתוני הטיסה ומחזירה אובייקט Camera שמייצג את השלב.

func makeKeyFrame(step: FlightPathLocation) -> CubicKeyframe<Camera> {
  return CubicKeyframe(
    makeCamera(step: step),
    duration: flyToDuration
  )
}

func makeCamera(step: FlightPathLocation) -> Camera {
  return .init(
    latitude: step.latitude,
    longitude: step.longitude,
    altitude: step.altitude,
    heading: step.bearing,
    tilt: 75,
    roll: 0,
    range: 200
  )
}

איך משלבים את האנימציה

קוראים ל-keyframeAnimator אחרי האיניציאליזציה של Map ומגדירים את הערכים הראשוניים.

תצטרכו מצב מצלמה ראשוני שמבוסס על המיקום הראשון בנתיב הטיסה.

האנימציה צריכה להופעל על סמך מצב משתנה שמשתנה.

התוכן של keyframeAnimator צריך להיות מפה.

רשימת ה-keyframes בפועל נוצרת על ידי חזרה על כל מיקום בנתיב הטיסה.

   VStack {
      Map(camera: $camera, mode: .hybrid)
        .keyframeAnimator(
          initialValue: makeCamera(step: flightData.flightPathData.flight[0]),
          trigger: animation,
          content: { view, value in
            Map(camera: .constant(value), mode: .hybrid)
          },
          keyframes: { _ in
            KeyframeTrack(content: {
              for i in  1...flightData.flightPathData.flight.count-1 {
                makeKeyFrame(step: flightData.flightPathData.flight[i])
              }
            })
          }
        )
   }

מציגים תצוגה מקדימה של האפליקציה ומריצים אותה.

פותחים את חלונית התצוגה המקדימה כדי לראות תצוגה מקדימה של התצוגה.

מוסיפים NavigationLink חדש עם יעד FlightPathDemo() אל GoogleMaps3DDemoApp.swift ומריצים את האפליקציה כדי לנסות אותה.

11. מזל טוב

יצרתם אפליקציה ש:

  • הוספת מפה תלת-ממדית בסיסית לאפליקציה.
  • הוספת סמנים, קווים, פוליגונים ומודלים למפה.
  • הטמעת קוד לשליטה במצלמה כדי לעבור מעל המפה וסביב מיקומים ספציפיים.

מה למדתם

  • איך מוסיפים את החבילה GoogleMaps3D לאפליקציית Xcode SwiftUI.
  • איך מאתחלים מפה תלת-ממדית באמצעות מפתח API ותצוגת ברירת מחדל.
  • איך מוסיפים למפה סמנים, מודלים תלת-ממדיים, קווים ופוליגונים.
  • איך לשלוט במצלמה כדי ליצור אנימציה של תנועה למיקום אחר.
  • איך מטפלים באירועי קליקים על סמני מיקום.

מה השלב הבא?

  • במדריך למפתחים מפורט מה אפשר לעשות באמצעות Maps 3D SDK ל-iOS.
  • כדי שנוכל ליצור את התוכן הכי שימושי עבורך, נשמח לקבל ממך תשובות לסקר הבא:

אילו Codelabs נוספים הייתם רוצים לראות?

הצגה חזותית של נתונים במפות מידע נוסף על התאמה אישית של הסגנון של המפות שלי פיתוח אינטראקציות תלת-ממדיות במפות