Build your first 3D map with SwiftUI

1. Before you begin

This codelab teaches you how to create a 3D maps app in SwiftUI using the Maps 3D SDK for iOS.

App showing a 3D map of San Francisco

You will learn:

  • How to control the camera to view locations and fly around the map.
  • How to add markers and models
  • How to draw lines and polygons
  • How to handle user clicks on Place markers.

Prerequisites

  • A Google Console project with billing enabled
  • An API key, optionally restricted to the Maps 3D SDK for iOS.
  • Basic knowledge of iOS development with SwiftUI.

What you'll do

  • Set up Xcode and bring in the SDK using Swift Package Manager
  • Configure your app to use an API key
  • Add a basic 3D Map to your app
  • Control the camera to fly to and around specific locations
  • Add markers, lines, polygons and models to your map

What you'll need

  • Xcode 15 or later.

2. Get set up

For the following enablement step, you'll need to enable the Maps 3D SDK for iOS.

Set up Google Maps Platform

If you do not already have a Google Cloud Platform account and a project with billing enabled, please see the Getting Started with Google Maps Platform guide to create a billing account and a project.

  1. In the Cloud Console, click the project drop-down menu and select the project that you want to use for this codelab.

  1. Enable the Google Maps Platform APIs and SDKs required for this codelab in the Google Cloud Marketplace. To do so, follow the steps in this video or this documentation.
  2. Generate an API key in the Credentials page of Cloud Console. You can follow the steps in this video or this documentation. All requests to Google Maps Platform require an API key.

Enable the Maps 3D SDK for iOS

You can find the Maps 3D SDK for iOS by using the Google Maps Platform > APIs and Services menu link in the console.

Click Enable to enable the API on your selected project.

Enable the Maps 3D SDK in Google Console

3. Create a basic SwiftUI app

Note: You can find solution code for each step in the codelab sample app repo on GitHub .

Create a new app in Xcode.

Code for this step can be found in the folder GoogleMaps3DDemo on GitHub.

Open Xcode and create a new app. Specify SwiftUI.

Call your app GoogleMaps3DDemo, with a package name com.example.GoogleMaps3DDemo.

Import the GoogleMaps3D library to your project

Add the SDK to your project using Swift Package Manager.

In your Xcode project or workspace, go to File > Add Package Dependencies. Enter https://github.com/googlemaps/ios-maps-3d-sdk as the URL, press Enter to pull in the package, and click "Add Package".

From the Choose Package Products window, verify that GoogleMaps3D will be added to your designated main target. Once complete, click Add Package.

To verify your installation, navigate to your target's General pane. In Frameworks, Libraries, and Embedded Content, you should see the installed packages. You can also view the Package Dependencies section of Project Navigator to verify the package and its version.

Add your API key

You can hard code your API key into the app, but this is not good practice. Adding a config file lets you keep your API Key secret and avoids checking it into source control.

Create a new config file in the project root folder

In Xcode, make sure you are viewing the project explorer window. Right-click the project root and select "New File from Template". Scroll until you see "Configuration Settings File". Select this and click "Next". Name the file Config.xcconfig and make sure the project root folder is selected. Click "Create" to create the file.

In the editor, add a line to the config file as follows: MAPS_API_KEY = YOUR_API_KEY

Replace YOUR_API_KEY with your API key.

Add this setting to Info.plist.

To do this, select the project root and click the "Info" tab.

Add a new property called MAPS_API_KEY with a value of $(MAPS_API_KEY).

The sample app code has an Info.plist file that specifies this property.

Add a map

Open the file called GoogleMaps3DDemoApp.swift. This is the entry point and main navigation for your app.

It calls ContentView() which shows a Hello World message.

Open ContentView.swift in the editor.

Add an import statement for GoogleMaps3D.

Delete the code inside the var body: some View {} code block. Declare a new Map() inside the body.

The minimum configuration you need to initialize a Map is a MapMode. This has two possible values:

  • .hybrid - satellite images with roads and labels, or
  • .satellite - satellite images only.

Choose .hybrid.

Your ContentView.swift file should look like this.

import GoogleMaps3D
import SwiftUI

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

Set your API key.

The API key must be set before the Map initializes.

You can do this by setting Map.apiKey in the init() event handler of any View that contains a Map. You can also set it in GoogleMaps3DDemoApp.swift before it calls ContentView().

In GoogleMaps3DDemoApp.swift, set Map.apiKey in the onAppear event handler of the WindowGroup.

Fetch your API key from the config file

Use Bundle.main.infoDictionary to access the MAPS_API_KEY setting you created in your config file.

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
    }
  }
}

Build and run your app to check it loads correctly. You should see a map of the globe.

3D map showing the earth

4. Use a camera to control the map view

Create a camera state object

3D Map views are controlled by the Camera class. In this step you will learn to specify the location, altitude, heading, tilt, roll and range to customize the map view.

3D map view of San Francisco

Create a Helpers class to store Camera settings

Add a new empty file called MapHelpers.swift. In your new file, import GoogleMaps3D, and add an extension to the Camera class. Add a variable called sanFrancisco. Initialize this variable as a new Camera object. Locate the camera at latitude: 37.39, longitude: -122.08.

import GoogleMaps3D

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

Add a new View to your App

Create a new file called CameraDemo.swift. Add the basic outline of a new SwiftUI View to the file.

Add a @State variable called camera of type Camera. Initialize it to the sanFrancisco camera you just defined.

Using @State lets you bind the Map to the camera state and use it as the source of truth.

@State var camera: Camera = .sanFrancisco

Change the Map() function call to include a camera property. Use the camera state binding $camera to initialise the camera property to your Camera @State object (.sanFrancisco).

import SwiftUI
import GoogleMaps3D

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

Add basic navigation UI to your app

Add a NavigationView to the main app entry point, GoogleMaps3DDemoApp.swift.

This will allow users to see a list of demos and click each one to open it.

Edit GoogleMaps3DDemoApp.swift to add a new NavigationView.

Add a List containing two NavigationLink declarations.

The first NavigationLink should open ContentView() with a Text description Basic Map.

The second NavigationLink should open CameraDemo().

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

Add an Xcode Preview

Previews are a powerful Xcode feature that let you see and interact with your app as you make changes to it.

To add a preview, open CameraDemo.swift. Add a #Preview {} code block outside the struct.

#Preview {
 CameraDemo()
}

Open or refresh the Preview pane in Xcode. The map should show San Francisco.

Set up custom 3D views

You can specify additional parameters to control the camera:

  • heading: the bearing in degrees from north to point the camera towards.
  • tilt: the angle of tilt in degrees, where 0 is directly overhead and 90 is looking horizontally.
  • roll: the angle of roll around the vertical plane of the camera, in degrees
  • range: the distance in metres of the camera from the latitude, longitude location
  • altitude: the height of the camera above sea level

If you don't supply any of these additional parameters, default values will be used.

To make the camera view show more 3D data, set the initial parameters to show a closer, tilted view.

Edit the Camera you defined in MapHelpers.swift to include values for altitude, heading, tilt, roll and 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)

Build and run the app to see and explore the new 3D view.

5. Basic camera animations

So far you have used the camera to specify a single location with a tilt, altitude, heading and range. In this step you will learn how to move the camera view by animating these properties from an initial state to a new state.

3D map of Seattle

Fly to a location

You will use the Map.flyCameraTo() method to animate the camera from the initial location to a new location.

The flyCameraTo() method takes a number of parameters:

  • a Camera representing the end location.
  • duration: How long the animation will run, in seconds.
  • trigger: an observable object that will trigger the animation when its state changes.
  • completion: code that will be executed when the animation completes.

Define a location to fly to

Open your MapHelpers.swift file.

Define a new camera object to show Seattle.

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

Add a button to trigger the animation.

Open your CameraDemo.swift. Declare a new Boolean variable inside the struct.

Call it animate with an initial value of false.

@State private var animate: Bool = false

Add a Button below the VStack. The Button will initiate the map animation.

Give the Button some appropriate Text such as "Start Flying".

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") {
      }
    }
  }
}

In the Button closure add code to toggle the state of the animate variable.

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

Start the animation.

Add the code to trigger the flyCameraTo() animation when the state of the animate variable changes.

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

Fly around a location

Flying around a location can be achieved using the Map.flyCameraAround() method. This method takes several parameters:

  • a Camera defining the location and view.
  • a duration in seconds.
  • rounds: the number of times to repeat the animation.
  • trigger: an observable object that will trigger the animation.
  • callback: code that will execute when the animation runs.

Define a new @State variable called flyAround, with an initial value of false.

When you have done that, add a call to flyCameraAround() immediately after the flyCameraTo() method call.

The fly around duration should be relatively long so that the view changes smoothly.

Make sure to trigger the flyCameraAround() animation by changing the state of the trigger object when flyCameraTo() completes.

Your code should look like this.

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()
}

Preview or run the app to see that the camera flies around the destination once the flyCameraTo() animation completes.

6. Add a Marker to your map.

In this step you will learn how to draw a marker pin on the map.

You will create a Marker object and add it to your map. The SDK will use a default icon for the marker. Finally you will adjust the marker altitude and other properties to change how it displays.

3D map showing a map marker

Create a new SwiftUI View for your Marker demo.

Add a new Swift file to your project. Call it MarkerDemo.swift.

Add the outline of a SwiftUI View and initialize the map as you did in your CameraDemo.

import SwiftUI
import GoogleMaps3D

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

Initialize a Marker object

Declare a new marker variable called mapMarker. At the top of the struct code block in MarkerDemo.swift.

Place the definition on the line below your camera declaration. This sample code initializes all available properties.

  @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"
  )

Add the Marker to your map.

To draw the marker, add it to a closure called when the Map is created.

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

Add a new NavigationLink to GoogleMaps3DDemoApp.swift with a destination of MarkerDemo() and Text describing it as "Marker Demo".

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

Preview and run your App

Refresh the preview or run your app to see the marker.

Extruded Markers

Markers can be placed above the ground or the 3D mesh using altitude and altitudeMode.

Copy the mapMarker declaration in MarkerDemo.swift to a new Marker variable called extrudedMarker.

Set a non-zero value for altitude, 50 is sufficient.

Change the altitudeMode to .relativeToMesh, and set extruded to true. Use the latitude and longitude from the code snippet here to place the marker on top of a skyscraper.

  @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"
  )

Run or preview the app again. The marker should appear on top of a 3D building.

7. Add a model to your map.

A Model can be added in the same way as a Marker. You will need a model file that can be accessed by URL or added as a local file in your project. For this step we will use a local file which you can download from the GitHub repository for this codelab.

3D map of San Francisco with a hot air balloon model

Add a model file to your project

Create a new folder in your Xcode project called Models.

Download the model from the GitHub sample app repository. Add it to your project by dragging it to the new folder in Xcode project view.

Make sure you set the target to be the main target for your app.

Check the Build Phases > Copy Bundle Resources settings for your project. The model file should be in the list of resources copied to the bundle. If it's not there click "+" to add it.

Add the model to your app.

Create a new SwiftUI file called ModelDemo.swift.

Add import statements for SwiftUI and GoogleMaps3D as in the previous steps.

Declare a Map inside a VStack in your body.

import SwiftUI
import GoogleMaps3D

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

Get the model path from your Bundle. Add the code for this outside the struct.

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

Declare a variable for your model inside the struct.

Supply a default value in case fileUrl is not supplied.

  @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. Use the model with your Map.

As with adding a Marker, just supply the reference to your Model in the Map declaration.

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

Preview and run your app

Add a new NavigationLink to GoogleMaps3DDemoApp.swift, with a destination of ModelDemo() and the Text "Model Demo".

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

Refresh the preview or run your app to see the model.

8. Draw a line and a polygon on your map.

In this step you will learn how to add lines and polygon shapes to your 3D map.

For simplicity you will define the shapes as arrays of LatLngAltitude objects. In a real application the data might be loaded from a file, an API call or a database.

3D map of San Francisco showing two polygons and a polyline

Create some shape objects to manage the shape data.

Add a new Camera definition to MapHelpers.swift that looks at downtown San Francisco.

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

Add a new file to your project called ShapesDemo.swift. Add a struct called ShapesDemo that implements the View protocol, and add a body to it.

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

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

      }
    }
  }
}

The classes you will use to manage shape data are Polyline and Polygon. Open ShapesDemo.swift and add them to the struct as follows.

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) )

Notice the initialization parameters used.

  • altitudeMode: .relativeToGround is used to extrude the polygons to a specific height above the ground.
  • altitudeMode: .clampToGround is used to make the polyline follow the shape of the earth's surface.
  • styles are set on the Polygon objects by chaining a method call to styleOptions() after .init() is called

Add the shapes to the Map

Just like in the previous steps, the shapes can be directly added to the Map closure. Create your Map inside a VStack.

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

Preview and run your App

Add Preview code and inspect your app in the Preview pane in Xcode.

#Preview {
  ShapesDemo()
}

To run your app, add a new NavigationLink to GoogleMaps3DDemoApp.swift that opens the new demo View.

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

Run your app and explore the shapes you added.

9. Handle tap events on Place markers

In this step you will learn how to respond to user taps on Place markers.

Map showing a pop up window with a Place ID

Note: To see Place markers on the map, you will need to set the MapMode to .hybrid.

Handling the tap requires implementing the Map.onPlaceTap method.

The onPlaceTap event provides a PlaceTapInfo object from which you can obtain the Place ID of the tapped Place marker.

You can use the Place ID to look up further details using the Places SDK or Places API.

Add a new Swift View

Add the following code to a new Swift file called 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()
}

Preview and run your App

Open the Preview pane to preview the app.

To run the app, add a new NavigationLink to GoogleMaps3DDemoApp.swift.

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

10. (Optional) Take it further

Advanced Camera animations

Some use cases require animating smoothly along a sequence or list of locations or camera states, for example a flight simulator or replaying a hike or run.

In this step you will learn how to load a list of locations from a file, and animate through each location in sequence.

3D map view of the approach to Innsbruck

Load a file containing a sequence of locations.

Download flightpath.json from the GitHub sample app repo.

Create a new folder in your Xcode project called JSON.

Drag flightpath.json to your JSON folder in Xcode.

Set the target to be your app's main target. Check that your project Copy Bundle Resources settings include this file.

Create two new Swift files in your app called FlightPathData.swift and FlightDataLoader.swift.

Copy the following code into your app. This code creates structs and classes that read a local file called "flighpath.json" and decode it as JSON.

The FlightPathData and FlightPathLocation structs represent the data structure in the JSON file as Swift objects.

The FlightDataLoader class reads the data from the file and decodes it. It adopts the ObservableObject protocol to allow your app to observe changes to its data.

The parsed data is exposed through a published property.

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.")
        }
      }
    }
  }
}

Animate the camera along each location

To animate the camera between a sequence of steps, you will use a KeyframeAnimator.

Each Keyframe will be created as a CubicKeyframe, so the changes in camera state are smoothly animated.

Using flyCameraTo() would cause the view to "bounce" between each location.

First of all declare a camera called "innsbruck" in 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)

Now set up a new View in a new file called FlyAlongRoute.swift.

Import SwiftUI and GoogleMaps3D. Add a Map and a Button inside a VStack. Set up the Button to toggle the state of the Boolean animation variable.

Declare a State object for FlightDataLoader, which will load the JSON file when it is initialized.

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()
      }
    }
  }
}

Create the Keyframes

The basic process is to create a function that returns a new frame in the animation sequence. Each new frame defines the next camera state for the animator to animate to. Once this function is created, call it with each location from the file in sequence.

Add two functions to your FlyAlongRoute struct. The function makeKeyFrame returns a CubicKeyframe with a Camera state. The function makeCamera takes a step in the flight data sequence and returns a Camera object representing the step.

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
  )
}

Put the animation together

Call the keyframeAnimator after Map initialization and set the initial values.

You will need an initial camera state based on the first location in the flight path.

The animation should trigger based on a variable changing state.

The keyframeAnimator content should be a Map.

The actual list of keyframes is generated by looping through each location in the flight path.

   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])
              }
            })
          }
        )
   }

Preview and run your app.

Open the Preview pane to preview your View.

Add a new NavigationLink with a destination of FlightPathDemo() to GoogleMaps3DDemoApp.swift and run the app to try it out.

11. Congratulations

You've successfully built an application that

  • Adds a basic 3D Map to your app.
  • Adds markers, lines, polygons and models to your map.
  • Implements code to control the camera to fly through the map and around specific locations.

What you learned

  • How to add the GoogleMaps3D package to an Xcode SwiftUI App.
  • How to initialize a 3D Map with an API Key and a default view.
  • How to add markers, 3D models, lines and polygons to your map.
  • How to control the camera to animate movement to another location.
  • How to handle click events on Place markers.

What's next?

  • Check out the developer guide for more details on what you can do with the Maps 3D SDK for iOS.
  • Help us create the content that you would find most useful by answering the following survey:

What other codelabs would you like to see?

Data visualization on maps More about customizing the style of my maps Building for 3D interactions in maps