Update the orientation of a graphic using expressions based on its attributes.
Use case
Instead of reading the attribute and changing the rotation on the symbol for a single graphic (a manual CPU operation), you can bind the rotation to an expression that applies to the whole overlay (an automatic GPU operation). This usually results in a noticeable performance boost (smooth rotations).
How to use the sample
Adjust the heading and pitch sliders to rotate the cone.
How it works
- Create a new graphics overlay.
- Create a simple renderer and set its scene properties.
- Set the heading expression to
[HEADING]
. - Apply the renderer to the graphics overlay.
- Create a graphic and add it to the overlay.
- To update the graphic's rotation, update the
HEADING
orPITCH
property in the graphic's attributes.
Relevant API
- Graphic.attributes
- GraphicsOverlay
- SceneProperties
- SceneProperties.headingExpression
- SceneProperties.pitchExpression
- SimpleRenderer
- SimpleRenderer.sceneProperties
Tags
3D, expression, graphics, heading, pitch, rotation, scene, symbology
Sample Code
ApplyScenePropertyExpressionsView.swift
// Copyright 2025 Esri // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import ArcGIS import SwiftUI struct ApplyScenePropertyExpressionsView: View { /// The scene that displays in the scene view. @State private var scene: ArcGIS.Scene = { let scene = Scene(basemapStyle: .arcGISImageryStandard) // Give the scene an initial viewpoint. let point = Point(x: 83.9, y: 28.4, z: 1000, spatialReference: .wgs84) scene.initialViewpoint = Viewpoint( latitude: .nan, longitude: .nan, scale: .nan, camera: .init(lookingAt: point, distance: 1000, heading: 0, pitch: 50, roll: 0) ) return scene }() /// The graphics overlay that we will display a cone graphic in. @State private var graphicsOverlay = { // Create a graphics overlay to hold our graphic. let overlay = GraphicsOverlay() overlay.sceneProperties.surfacePlacement = .relative // Create a renderer for our graphics overlay and setup the heading // and pitch expressions that will be used to adjust the heading // and pitch of each graphic in the overlay. These expressions will // be calculated based on the corresponding values in the graphic's // attribute dictionary. let renderer = SimpleRenderer() renderer.sceneProperties.headingExpression = "[HEADING]" renderer.sceneProperties.pitchExpression = "[PITCH]" overlay.renderer = renderer // Create a cone symbol. let symbol = SimpleMarkerSceneSymbol.cone( color: .red, diameter: 100, height: 100 ) // Create a graphic, setting initial heading and pitch in the // attributes. let graphic = Graphic( geometry: Point(x: 83.9, y: 28.42, z: 200, spatialReference: .wgs84), attributes: [ "HEADING": 180.0, "PITCH": 45.0 ], symbol: symbol ) // Add the graphic to the overlay and return the overlay. overlay.addGraphic(graphic) return overlay }() /// A Boolean value indicating if the settings pane is displayed. @State private var isSettingsPresented = false /// The heading of the cone. @State private var heading = 0.0 /// The pitch of the cone. @State private var pitch = 0.0 /// The cone graphic. private var coneGraphic: Graphic { graphicsOverlay.graphics[0] } var body: some View { SceneView( scene: scene, graphicsOverlays: [graphicsOverlay] ) .toolbar { ToolbarItem(placement: .bottomBar) { Button("Settings") { isSettingsPresented = true } .popover(isPresented: $isSettingsPresented) { NavigationStack { // The settings pane to adjust the symbology heading and pitch. Form { Section { LabeledContent("Heading", value: heading, format: .number) Slider(value: $heading, in: 0...360, step: 1) { Text("Heading") } minimumValueLabel: { Text("0") } maximumValueLabel: { Text("360") } } Section { LabeledContent("Pitch", value: pitch, format: .number) Slider(value: $pitch, in: 0...180, step: 1) { Text("Pitch") } minimumValueLabel: { Text("0") } maximumValueLabel: { Text("180") } } } .toolbar { ToolbarItem(placement: .confirmationAction) { Button("Done") { isSettingsPresented = false } } } .navigationTitle("Expression Settings") .navigationBarTitleDisplayMode(.inline) } .presentationDetents([.medium]) .frame(idealWidth: 320, idealHeight: 380) } } } .onAppear { // Sync view state with the graphic attributes on startup. heading = coneGraphic.attributes["HEADING"] as? Double ?? 0 pitch = coneGraphic.attributes["PITCH"] as? Double ?? 0 } // Sync view state with the graphic attributes on as it changes. .onChange(of: heading) { coneGraphic.setAttributeValue(heading, forKey: "HEADING") } .onChange(of: pitch) { coneGraphic.setAttributeValue(pitch, forKey: "PITCH") } } } #Preview { ApplyScenePropertyExpressionsView() }