透過 SwiftUI (Swift) 在 iOS 應用程式中加入地圖

1. 事前準備

本程式碼研究室將教導您如何搭配 Mapsif for iOS 使用 SwiftUI。

螢幕擷取畫面-iphone-12-black@2x.png

必要條件

  • Swift 基本知識
  • 對 SwiftUI 的基本概念

執行步驟

  • 啟用並使用 Maps SDK for iOS,使用 SwiftUI 將 Google 地圖新增至 iOS 應用程式。
  • 在地圖上加入標記。
  • 將 SwiftUI 檢視的狀態傳遞至 GMSMapView 物件,反之亦然。

軟硬體需求

2. 做好準備

在下列啟用步驟中,啟用 Maps SDK for iOS

設定 Google 地圖平台

如果您還沒有 Google Cloud Platform 帳戶和已啟用計費功能的專案,請參閱開始使用 Google 地圖平台指南,建立帳單帳戶和專案。

  1. Cloud Console 中按一下專案下拉式選單,然後選取您要用於這個程式碼研究室的專案。

  1. Google Cloud Marketplace 中啟用此程式碼研究室所需的 Google 地圖平台 API 和 SDK。詳細步驟請參閱這部影片這份文件
  2. 在 Cloud Console 的「憑證」頁面中產生 API 金鑰。你可以按照這部影片這份說明文件中的步驟進行。傳送至 Google 地圖平台的所有要求都需要 API 金鑰。

3. 下載範例程式碼

以下提供一些入門程式碼,協助您快速上手,幫助您快速上手。我們決定直接跳到解決方案,但如果您想依照自己的所有步驟逐步進行,請繼續閱讀本文。

  1. 如果您已安裝 git,請複製存放區。
git clone https://github.com/googlecodelabs/maps-ios-swiftui.git

或者,您也可以點擊下方按鈕來下載原始碼。

  1. 收到驗證碼後,在終端機 cd 進入 starter/GoogleMapsSwiftUI 字典。
  2. 執行 carthage update --platform iOS 即可下載 Maps SDK for iOS
  3. 最後,請在 Xcode 中開啟 GoogleMapsSwiftUI.xcodeproj 檔案

4. 程式碼總覽

在您下載的入門專案中,我們為您提供了並實作下列類別:

  • AppDelegate - 應用程式的UIApplicationDelegate。而 Maps SDK for iOS 將初始化。
  • City - 代表城市的結構 (包含城市的名稱和座標)。
  • MapViewController - 包含 UI 的簡單 UIKit UIViewController (GMSMapView)
  • SceneDelegate - 用於執行 ContentView 的應用程式 UIWindowSceneDelegate

此外,下列類別具有部分導入,將由您透過本程式碼研究室結束:

  • ContentView:包含應用程式的頂層 SwiftUI 檢視。
  • MapViewControllerBridge:這個類別可將 UIKit 檢視連結到 SwiftUI 檢視。具體而言,這門課程能讓 MapViewController 在 SwiftUI 中存取。

5. 使用 SwiftUI 與 UIKit 的比較

SwiftUI 是在 iOS 13 中推出,用來替代 iOS 應用程式的開發使用者介面,而非 UIKit。與前一版的 UIKit 相比,SwiftUI 有許多優點。如何命名幾個項目:

  • 狀態會隨著狀態改變而自動更新。使用稱為 State 的物件,只要物件所含的值有所變更,使用者介面就會自動更新。
  • 即時預覽可以加快開發速度。即時預覽功能可以降低建構與部署程式碼到模擬器的作業流程,因為 Xcode 可以清楚顯示 SwiftUI 視圖的預覽畫面。
  • 資料來源為 Swift。系統會在 Swift 中宣告所有 SwiftUI 檢視,因此不再需要使用介面製作工具。
  • 與 UIKit 互通。與 UIKit 互通,可確保現有應用程式能夠逐步使用 SwiftUI 和現有資料檢視。此外,尚未支援 SwiftUI 的程式庫 (例如 Maps SDK for iOS) 仍可在 SwiftUI 中使用。

部分缺點:

  • SwiftUI 僅適用於 iOS 13 以上版本。
  • 無法在 Xcode 預覽中檢查檢視階層。

SwiftUI 狀態和資料流程

SwiftUI 提供使用宣告式方法建立 UI 的全新方式,您可以告訴 SwiftUI 您希望檢視畫面的外觀及其他不同狀態,然後由系統代勞。SwiftUI 會根據事件或使用者動作,在基礎狀態變更時處理更新。這個設計通常稱為「單向資料流程」。雖然此程式碼的細節不在這個程式碼研究室的範圍內,但仍建議您閱讀有關 Apple 的狀態與資料流程說明文件,瞭解這項功能的運作方式。

使用 UIViewRepresentable 或 UIViewControllerRepresentable 處理 UIKit 和 SwiftUI

由於 Maps SDK for iOS 是以 UIKit 為基礎建立而成,因此尚未提供與 SwiftUI 相容的檢視選項,因此如要使用 SwiftUI,就必須符合 UIViewRepresentableUIViewControllerRepresentable 的要求。這些通訊協定可讓 SwiftUI 分別加入 UIKit 建構的 UIViewUIViewController。雖然您可以使用任一通訊協定將 Google 地圖加入 SwiftUI 檢視,但在下一個步驟中,我們來看看如何使用 UIViewControllerRepresentable 來包含包含地圖的 UIViewController

6. 新增地圖

在本節中,您會將 Google 地圖新增到 SwiftUI 檢視。

add-a-map-螢幕擷取畫面@2x.png

新增 API 金鑰

您在先前步驟中建立的 API 金鑰必須提供給 Maps SDK for iOS,才能將您的帳戶與應用程式中顯示的地圖建立關聯。

如要提供 API 金鑰,請開啟 AppDelegate.swift 檔案並前往 application(_, didFinishLaunchingWithOptions) 方法。目前,SDK 是透過 GMSServices.provideAPIKey() 初始化,字串為「YOUR_API_KEY」。然後將該字串替換成您的 API 金鑰。完成此步驟後,應用程式啟動時,Maps SDK for iOS 將初始化。

使用 MapViewControllerBridge 新增 Google 地圖

現在您的 API 金鑰已提供給 SDK,接下來要在應用程式中顯示地圖。

在範例程式碼中提供的檢視控制項「MapViewController」目前含有一個 GMSMapView。不過,由於這個檢視控制項是在 UIKit 中建立,所以您必須將這個類別連結到 SwiftUI,才能在 ContentView 中使用。方法如下:

  1. 在 Xcode 中開啟「MapViewControllerBridge」檔案。

此類別符合 UIViewControllerRepresentable,這是將 UIKit UIViewController 包裝所需的通訊協定,以便做為 SwiftUI 檢視使用。換句話說,只要遵循此通訊協定,您就能將 UIKit 檢視連結到 SwiftUI 檢視。符合此通訊協定需要實作兩種方法:

  • makeUIViewController(context) - SwiftUI 會呼叫這個方法來建立基礎 UIViewController。您可以在這裡為 UIViewController 執行個體化,並傳送其初始狀態。
  • updateUIViewController(_, context) - SwiftUI 會在狀態改變時呼叫這個方法。您可以在這裡修改基礎 UIViewController,以因應狀態變更。
  1. 建立 MapViewController

makeUIViewController(context) 函式中,將新的 MapViewController 執行個體化並傳回結果。完成上述步驟後,您的 MapViewControllerBridge 應該會如下所示:

MapViewControllerBridge

import GoogleMaps
import SwiftUI

struct MapViewControllerBridge: UIViewControllerRepresentable {

  func makeUIViewController(context: Context) -> MapViewController {
    return MapViewController()
  }

  func updateUIViewController(_ uiViewController: MapViewController, context: Context) {
  }
}

在 ContentView 中使用 MapViewControllerBridge

MapViewControllerBridge 現在建立的是 MapViewController 的例項,下一步則是在 ContentView 中使用此結構以顯示地圖。

  1. 在 Xcode 中開啟「ContentView」檔案。

ContentView 已在 SceneDelegate 中執行個體化,其中包含頂層應用程式檢視。系統將在這個檔案中新增地圖。

  1. body 屬性中建立 MapViewControllerBridge

在這個檔案的 body 屬性中,系統已為您提供了並實作了 ZStackZStack 目前包含可互動和拖曳的城市清單,您可以在後續步驟中使用。目前,在 ZStack 內建立 MapViewControllerBridge 時,會成為 ZStack 的第一個子檢視,因此地圖會隨即顯示在城市檢視清單後方的應用程式。執行此動作時,ContentViewbody 屬性的內容應如下所示:

內容檢視

var body: some View {

  let scrollViewHeight: CGFloat = 80

  GeometryReader { geometry in
    ZStack(alignment: .top) {
      // Map
      MapViewControllerBridge()

      // Cities List
      CitiesList(markers: $markers) { (marker) in
        guard self.selectedMarker != marker else { return }
        self.selectedMarker = marker
        self.zoomInCenter = false
        self.expandList = false
      }  handleAction: {
        self.expandList.toggle()
      } // ...
    }
  }
}
  1. 現在,請直接執行應用程式吧。現在您的裝置畫面上應該將地圖載入量和可拖曳的城市清單朝向螢幕底部顯示。

7. 在地圖上加入標記

在上一個步驟中,您加入了一個可互動清單的地圖,上面有一份城市清單。在本節中,您將為清單中的每個城市新增標記。

map-with-Markers@2x.png

標記狀態

ContentView目前正在宣告名為 markers 的屬性,此清單為 GMSMarker,代表 cities 靜態屬性中宣告的每個城市。請注意,此屬性會以 SwiftUI 屬性包裝函式 State 加註,表示該屬性應由 SwiftUI 管理。因此,如果系統偵測到任何與此屬性有關的任何變更 (例如新增或移除標記),就會更新使用該狀態的檢視。

內容檢視

  static let cities = [
    City(name: "San Francisco", coordinate: CLLocationCoordinate2D(latitude: 37.7576, longitude: -122.4194)),
    City(name: "Seattle", coordinate: CLLocationCoordinate2D(latitude: 47.6131742, longitude: -122.4824903)),
    City(name: "Singapore", coordinate: CLLocationCoordinate2D(latitude: 1.3440852, longitude: 103.6836164)),
    City(name: "Sydney", coordinate: CLLocationCoordinate2D(latitude: -33.8473552, longitude: 150.6511076)),
    City(name: "Tokyo", coordinate: CLLocationCoordinate2D(latitude: 35.6684411, longitude: 139.6004407))
  ]

  /// State for markers displayed on the map for each city in `cities`
  @State var markers: [GMSMarker] = cities.map {
    let marker = GMSMarker(position: $0.coordinate)
    marker.title = $0.name
    return marker
  }

請注意,ContentView 會使用 markers 屬性將城市傳遞至 CitiesList 類別,以顯示城市清單。

城市清單

struct CitiesList: View {

  @Binding var markers: [GMSMarker]

  var body: some View {
    GeometryReader { geometry in
      VStack(spacing: 0) {
        // ...
        // List of Cities
        List {
          ForEach(0..<self.markers.count) { id in
            let marker = self.markers[id]
            Button(action: {
              buttonAction(marker)
            }) {
              Text(marker.title ?? "")
            }
          }
        }.frame(maxWidth: .infinity)
      }
    }
  }
}

透過 繫結,將狀態傳送到 MapViewControllerBridge

除了顯示 markers 屬性資料的城市清單以外,請將這個屬性傳送至 MapViewControllerBridge 結構,讓該物件能用來在地圖上顯示這些標記。請按照下列步驟操作:

  1. MapViewControllerBridge 中宣告具有 @Binding 註解的新 markers 屬性

MapViewControllerBridge

struct MapViewControllerBridge: : UIViewControllerRepresentable {
  @Binding var markers: [GMSMarker]
  // ...
}
  1. MapViewControllerBridge 中,更新 updateUIViewController(_, context) 方法以使用 markers 屬性

如先前步驟所述,SwiftUI 會在狀態變更時呼叫 updateUIViewController(_, context)。這個方法是我們會更新地圖,因此要在 markers 中顯示標記。如要這麼做,請先更新每個標記的 map 屬性。完成這個步驟後,您的 MapViewControllerBridge 看起來應該會像這樣:

import GoogleMaps
import SwiftUI

struct MapViewControllerBridge: UIViewControllerRepresentable {

  @Binding var markers: [GMSMarker]

  func makeUIViewController(context: Context) -> MapViewController {
    return MapViewController()
  }

  func updateUIViewController(_ uiViewController: MapViewController, context: Context) {
    // Update the map for each marker
    markers.forEach { $0.map = uiViewController.map }
  }
}
  1. markers 屬性從ContentView傳送到MapViewControllerBridge

由於您在 MapViewControllerBridge 中新增了屬性,現在此屬性的值必須透過 MapViewControllerBridge 的初始化工具傳遞。因此,如果您嘗試建構應用程式,請注意應用程式無法編譯。如要解決這個問題,請將 ContentView 更新為建立 MapViewControllerBridge 的地方,並傳遞 markers 屬性,如下所示:

struct ContentView: View {
  // ...
  var body: some View {
    // ...
    GeometryReader { geometry in
      ZStack(alignment: .top) {
        // Map
        MapViewControllerBridge(markers: $markers)
        // ...
      }
    }
  }
}

請注意,前置字串 $ 用來在 markers 中傳遞至 MapViewControllerBridge,因為這是預期屬性。$ 是與 Swift 屬性包裝函式搭配使用的保留前置字元。套用至某個州時,系統會傳回繫結

  1. 請直接執行應用程式,即可在地圖上顯示標記。

8. 為所選城市建立動畫

在上一個步驟中,您會將狀態從某個 SwiftUI 檢視傳送至另一個,以在地圖中加入標記。在這個步驟中,您將需要在可互動清單中輕觸某個城市/標記。如要執行動畫,當變更時,您必須修改地圖的相機位置,藉此回應狀態。如要進一步瞭解地圖相機的概念,請參閱相機和檢視畫面

animate-city@2x.png

為選取的城市建立動畫地圖

如何將地圖設為所選城市:

  1. MapViewControllerBridge 中定義新繫結

ContentView 具有名為 selectedMarker 的 State 屬性,它將初始化為「nil」,並在清單中選取城市時更新。這會由 ContentView 中的 CitiesList 資料檢視 buttonAction 處理。

內容檢視

CitiesList(markers: $markers) { (marker) in
  guard self.selectedMarker != marker else { return }
  self.selectedMarker = marker
  // ...
}

selectedMarker 變更時,MapViewControllerBridge 應瞭解此狀態變更,以讓地圖為選取的標記製作動畫。因此,在 GMSMarkerMapViewControllerBridge 中定義新繫結並命名為 selectedMarker

MapViewControllerBridge

struct MapViewControllerBridge: UIViewControllerRepresentable {
  @Binding var selectedMarker: GMSMarker?
}
  1. 更新 MapViewControllerBridge,讓地圖在 selectedMarker 出現變化時建立動畫

宣告新的繫結後,您必須更新 MapViewControllerBridgeupdateUIViewController_, context) 函式,將地圖轉成選取的標記。請直接複製下方的程式碼,即可開始使用:

struct MapViewControllerBridge: UIViewControllerRepresentable {
  @Binding var selectedMarker: GMSMarker?

  func updateUIViewController(_ uiViewController: MapViewController, context: Context) {
    markers.forEach { $0.map = uiViewController.map }
    selectedMarker?.map = uiViewController.map
    animateToSelectedMarker(viewController: uiViewController)
  }

  private func animateToSelectedMarker(viewController: MapViewController) {
    guard let selectedMarker = selectedMarker else {
      return
    }

    let map = viewController.map
    if map.selectedMarker != selectedMarker {
      map.selectedMarker = selectedMarker
      DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
        map.animate(toZoom: kGMSMinZoomLevel)
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
          map.animate(with: GMSCameraUpdate.setTarget(selectedMarker.position))
          DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: {
            map.animate(toZoom: 12)
          })
        }
      }
    }
  }
}

animateToSelectedMarker(viewController) 函式會使用 GMSMapView's 的 animate(with) 函式執行一系列地圖動畫。

  1. ContentViewselectedMarker 傳送到 MapViewControllerBridge

MapViewControllerBridge 宣告新的繫結後,請更新 ContentView 以傳遞 selectedMarker,讓 MapViewControllerBridge 執行個體化。

內容檢視

struct ContentView: View {
  // ...
  var body: some View {
    // ...
    GeometryReader { geometry in
      ZStack(alignment: .top) {
        // Map
        MapViewControllerBridge(markers: $markers, selectedMarker: $selectedMarker)
        // ...
      }
    }
  }
}

完成此步驟後,每當您在清單中選取新的城市時,地圖就會為該動畫製作動畫。

動畫 SwiftUI 以欣賞城市美景

SwiftUI 可輕鬆處理「檢視狀態」動畫的動畫,因此檢視檢視畫面變得簡單。為了示範這一點,您將在地圖動畫完成之後,將檢視畫面移至所選城市,以新增更多動畫。為達成這個目標,請完成下列步驟:

  1. 將「onAnimationEnded」設為MapViewControllerBridge

由於 SwiftUI 動畫將在您先前新增的地圖動畫之後執行,請在 MapViewControllerBridge 中宣告名為 onAnimationEnded 的新關閉,並在 animateToSelectedMarker(viewController) 方法的最後一個地圖動畫延遲 0.5 秒後叫用這個關閉。

MapViewControllerBridge

struct MapViewControllerBridge: UIViewControllerRepresentable {
    var onAnimationEnded: () -> ()

    private func animateToSelectedMarker(viewController: MapViewController) {
    guard let selectedMarker = selectedMarker else {
      return
    }

    let map = viewController.map
    if map.selectedMarker != selectedMarker {
      map.selectedMarker = selectedMarker
      DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
        map.animate(toZoom: kGMSMinZoomLevel)
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
          map.animate(with: GMSCameraUpdate.setTarget(selectedMarker.position))
          DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: {
            map.animate(toZoom: 12)
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: {
              // Invoke onAnimationEnded() once the animation sequence completes
              onAnimationEnded()
            })
          })
        }
      }
    }
  }
}
  1. 在「MapViewControllerBridge」中導入onAnimationEnded

實作 onAnimationEnded 關閉,其中 MapViewControllerBridge 會在 ContentView 中執行個體化。複製並貼上以下程式碼,以新增名為 zoomInCenter 的新狀態,並利用 clipShape 來修改檢視區塊,並依據 zoomInCenter 的值調整裁剪形狀的直徑

內容檢視

struct ContentView: View {
  @State var zoomInCenter: Bool = false
  // ...
  var body: some View {
    // ...
    GeometryReader { geometry in
      ZStack(alignment: .top) {
        // Map
        let diameter = zoomInCenter ? geometry.size.width : (geometry.size.height * 2)
        MapViewControllerBridge(markers: $markers, selectedMarker: $selectedMarker, onAnimationEnded: {
          self.zoomInCenter = true
        })
        .clipShape(
           Circle()
             .size(
               width: diameter,
               height: diameter
             )
             .offset(
               CGPoint(
                 x: (geometry.size.width - diameter) / 2,
                 y: (geometry.size.height - diameter) / 2
               )
             )
        )
        .animation(.easeIn)
        .background(Color(red: 254.0/255.0, green: 1, blue: 220.0/255.0))
      }
    }
  }
}
  1. 現在就開始執行應用程式吧!

9. 傳送事件至 SwiftUI

在這個步驟中,您會監聽 GMSMapView 傳送的事件,然後將該事件傳送至 SwiftUI。具體而言,您將在「地圖檢視」中設定委派對象,並監聽「相機」移動事件,如此一來,當該城市以焦點為中心,而地圖相機從手勢移動後,地圖檢視就會聚焦,讓您查看更多地圖內容。

使用 SwiftUI 協調器

GMSMapView 會發出事件,例如攝影機位置變更或使用者輕觸標記時。監聽這些事件的機制是透過 GMSMapViewDelegate 通訊協定。SwiftUI 推出「Coordinator」的概念,這個概念專門用於 UIKit 檢視控制器的委派對象。因此,在 SwiftUI 世界中,協調人員應負責遵守 GMSMapViewDelegate 通訊協定。如要這樣做,請按照下列步驟進行:

  1. MapViewControllerBridge 中建立名稱為 MapViewCoordinator 的協調員

MapViewControllerBridge 類別中建立巢狀類別並呼叫 MapViewCoordinator。此類別應符合 GMSMapViewDelegate,且應宣告 MapViewControllerBridge 為屬性。

MapViewControllerBridge

struct MapViewControllerBridge: UIViewControllerRepresentable {
  // ...
  final class MapViewCoordinator: NSObject, GMSMapViewDelegate {
    var mapViewControllerBridge: MapViewControllerBridge

    init(_ mapViewControllerBridge: MapViewControllerBridge) {
      self.mapViewControllerBridge = mapViewControllerBridge
    }
  }
}
  1. 在「MapViewControllerBridge」中導入makeCoordinator()

接著,在 MapViewControllerBridge 中實作 makeCoordinator() 方法,並傳回您在前一步驟建立的 MapViewCoodinator 例項。

MapViewControllerBridge

struct MapViewControllerBridge: UIViewControllerRepresentable {
  // ...
  func makeCoordinator() -> MapViewCoordinator {
    return MapViewCoordinator(self)
  }
}
  1. MapViewCoordinator 設為地圖檢視

建立自訂協調人員後,下一步是將協調者設為檢視控制項地圖檢視的委任對象。如要這麼做,請在 makeUIViewController(context) 中更新檢視控制器初始化功能。您在上一個步驟中建立的協調器可透過 Context 物件存取。

MapViewControllerBridge

struct MapViewControllerBridge: UIViewControllerRepresentable {
  // ...
  func makeUIViewController(context: Context) -> MapViewController {
    let uiViewController = MapViewController()
    uiViewController.map.delegate = context.coordinator
    return uiViewController
  }
  1. 在「MapViewControllerBridge」中新增封閉式鏡頭後,攝影機就會移動活動

由於目標是要透過相機移動更新視圖,因此請宣告新的關閉屬性 (接受在 MapViewControllerBridge 中為「mapViewWillMove」的布林值),並在 MapViewCoordinator 的委派方法「mapView(_, willMove)」中叫用這個關閉方式。將 gesture 的值傳送到上鎖,這樣 SwiftUI 視圖只能對手勢相關的相機移動事件做出反應。

MapViewControllerBridge

struct MapViewControllerBridge: UIViewControllerRepresentable {
  var mapViewWillMove: (Bool) -> ()
  //...

  final class MapViewCoordinator: NSObject, GMSMapViewDelegate {
    // ...
    func mapView(_ mapView: GMSMapView, willMove gesture: Bool) {
      self.mapViewControllerBridge.mapViewWillMove(gesture)
    }
  }
}
  1. 更新 ContentView 以傳入 mapWillMove 的值

MapViewControllerBridge 上宣布新的結案後,請更新 ContentView 來傳遞此新的關閉值。如果在移動事件與手勢有關,請在這個關閉狀態下將狀態 zoomInCenter 切換為 false。如此一來,當地圖透過手勢移動時,就能重新顯示完整的檢視畫面。

內容檢視

struct ContentView: View {
  @State var zoomInCenter: Bool = false
  // ...
  var body: some View {
    // ...
    GeometryReader { geometry in
      ZStack(alignment: .top) {
        // Map
        let diameter = zoomInCenter ? geometry.size.width : (geometry.size.height * 2)
        MapViewControllerBridge(markers: $markers, selectedMarker: $selectedMarker, onAnimationEnded: {
          self.zoomInCenter = true
        }, mapViewWillMove: { (isGesture) in
          guard isGesture else { return }
          self.zoomInCenter = false
        })
        // ...
      }
    }
  }
}
  1. 現在就開始執行應用程式吧!

10. 恭喜

恭喜您獲得這樣的成果!您已涵蓋許多領域,希望您從中學到了,您現在可以運用 Maps SDK for iOS 建構自己的 SwiftUI 應用程式。

您學到的內容

後續步驟

  • Maps SDK for iOS - Maps SDK for iOS 的官方說明文件
  • Places SDK for iOS:尋找附近的商家和搜尋點
  • maps-sdk-for-ios-samples - GitHub 上的程式碼範例,示範 Maps SDK for iOS 的所有功能。
  • SwiftUI - Apple 的 SwiftUI 官方說明文件
  • 請回答下列問題,協助我們製作您覺得最實用的內容:

您還想查看其他程式碼研究室嗎?

在地圖上顯示資料視覺化 進一步瞭解如何自訂地圖樣式 在地圖上建立 3D 互動

上方未列出您所需的程式碼研究室嗎?請在這裡提出新的問題