Concurrent GeoView tasks

Concurrency is an integral part of the Swift framework that allows you to execute tasks in the background so that you can build highly responsive apps for your users. When you develop map view and scene view apps, you need to adopt asynchronous programming patterns for many workflows, such as loading resources, querying remote services, or downloading maps for offline use. These workflows can be addressed with either of the following task-related coding patterns:

  • .task view modifier: If you want to start loading remote resources, such as a map and its layers, before a map view appears, then use the map view's task view modifier. The task's lifetime matches the lifetime of its view.

  • Task block: If you want to execute long running work when the user interacts with the UI, by clicking on a button, tapping on the screen, or zooming the map, then use a Task block. A task block allows you to perform a range of asynchronous actions, such as querying a remote service feature table, starting a job to download an offline map, or performing a 3D analysis when the user taps on the scene view. You can also control the lifetime of a task block by canceling it programmatically.

Let's explore these two task coding patterns in detail.

Task view modifier

SwiftUI provides a task(priority:_:) view modifier that you can access from a MapView or a SceneView. You just need to add your asynchronous code into the task's closure and it will start to execute before the map view or scene view appears. This is particularly useful for loading the view's initial data, either from a remote service or a local data store.

In the code below, a task view modifier is added to MapView. Its closure contains code to asynchronously load a mobile map package, obtain the first map in the package, and use the map to create the MapView.

Use dark colors for code blocksCopy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Store the map in a state property so that it can be used by the MapView constructor.
@State private var map = Map()

var body: some View {
    MapView(map: map)

        // Creates a task view modifier that asynchronously executes the code
        // in its closure while the MapView is being created.
        .task {

            // Asynchronously load a mobile map package and obtain its first map.

            let mobileMapPackage = MobileMapPackage(fileURL: Bundle.main.url(forResource: "Yellowstone", withExtension: "mmpk")!)
            do {
                try await mobileMapPackage.load()
                guard let map = mobileMapPackage.maps.first else { return }
                self.map = map
            } catch {
                self.error = error
            }
        }
}

Keep in mind that the task view modifier's lifetime is tied to the lifetime of the view. This means that SwiftUI will cancel the task if the view disappears or its identity changes. A view can have multiple task view modifiers, to load different types of data for example. They are all run concurrently.

SwiftUI provides an overload of this task view modifier called the task(id:priority:_:). With this variant, you can observe a property that conforms to the Equatable protocol and execute the closures code. If the property's value changes, SwiftUI cancels the existing task and executes a new task using the new value.

In the code below, a task view modifier is added to the SceneView and it listens for any changes in the scenePoint property. Before the SceneView appears, the scenePoint value is nil, so the executing code returns. If the user taps on the scene view, the onSingleTapGesture view modifier stores the tapped point in the scenePoint property. This change in the scenePoint value triggers the task execution again. It now returns the scene's surface elevation at the updated scenePoint and shows it in a callout.

Use dark colors for code blocksCopy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@StateObject private var model = Model()

@State private var scenePoint: Point?
@State private var elevation: Double?
@State private var calloutPlacement: CalloutPlacement?

var body: some View {
    SceneView(scene: model.scene)

        // Creates a task view modifier that listens to any changes in the value of the scenePoint property.
        // If there is a change, the code executes asynchronously to get the scene's base surface elevation
        // at the scenePoint and shows it in a callout.
        .task(id: scenePoint) {
            guard let scenePoint else { return }
            elevation = try? await model.scene.baseSurface.elevation(at: scenePoint)
            calloutPlacement = CalloutPlacement.location(scenePoint)
        }

        // Updates the scene's point when the user taps on the scene view.
        .onSingleTapGesture { screenPoint, scenePoint in
            self.scenePoint = scenePoint
        }

        // Displays a callout at the scenePoint showing the elevation.
        .callout(placement: $calloutPlacement.animation(.default.speed(2))) { _ in
            VStack(alignment: .leading) {
                Text("Elevation")
                    .font(.headline)
                Text(model.elevationString(elevation: elevation))
                    .font(.callout)
            }
            .padding(5)
        }
    }

Similar to task view modifier, there are other view modifiers that provide asynchronous functionality associated with your map or scene views. For example, onDisappear(perform:) view modifier executes when the view's lifetime comes to an end, and can be used to clean up the data used by the view.

Task block

SwiftUI provides a Task block that can execute asynchronous functions at any time. These could be triggered by a user interaction with the view, such as a button click. Similar to the task view modifier, described in the previous section, you add asynchronous code into the task block closure.

In the code below, a Button labeled "Query City" is added to the ToolbarItem of the MapView's toolbar. When the user clicks the button, the Task is instantiated and its closure calls the queryCity(name:) method using the await keyword.

Use dark colors for code blocksCopy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var body: some View {

    // Create a state property to hold the handle to a Task.
    @State private var task: Task<Void, Never>?

    MapView(map: model.map)
        // Add a ToolbarItem to the MapView's toolbar view modifier.
        .toolbar {
            ToolbarItem(placement: .bottomBar) {
                // Add a Button to the toolbar.
                Button("Query City") {
                    // Assign the Task handle to the state property so that you can cancel it,
                    // if necessary.
                    task = Task {
                        // The Task closure calls the model's asynchronous queryCity function
                        // using the cities name.
                        try? await model.queryCity(name: "Los Angeles")
                    }
                }
            }
        }
        .onDisappear {
            // Cancel all functions in the task when the MapView disappears.
            task?.cancel()
            task = nil
        }
}

Unlike the task view modifier described above, the task block will continue to execute if the view disappears. If you want to cancel the task block, you must reference the instance. For example, in the code above, a reference to the Task instance is assigned to a @State task property. When the map view disappears, the onDisappear view modifier closure executes the task.cancel() method to terminate the task.

Samples

Identify layer features

Identify features in all layers in a map.

Add dynamic entity layer

Display data from an ArcGIS stream service using a dynamic entity layer.

Display clusters

Display a web map with a point feature layer that has feature reduction enabled to aggregate points into clusters.

Your browser is no longer supported. Please upgrade your browser for the best experience. See our browser deprecation post for more details.