> For the complete documentation index, see [llms.txt](https://docs.mapbox.com/ios/maps/llms.txt)

# SwiftUI - Interactive 3D model with feature state

![Use feature state expressions to control 3D model materials and node animations.](https://docs.mapbox.com/ios/assets/ideal-img/maps-examples-3D-model-iteractive.f98a6df.480.png)

This example demonstrates how to use feature state expressions to interactively control a 3D car model in a SwiftUI environment. Unlike the source-property approach, this method uses [`setFeatureState`](https://docs.mapbox.com/ios/maps/api/latest/documentation/mapboxmaps/mapboxmap/setfeaturestate(sourceid:sourcelayerid:featureid:state:callback:)/) to update individual feature states, which are then read by expressions on the [`ModelLayer`](https://docs.mapbox.com/ios/maps/api/latest/documentation/mapboxmaps/modellayer/).

The [`ModelSource`](https://docs.mapbox.com/ios/maps/api/latest/documentation/mapboxmaps/modelsource/) is configured with material and node override names that map to expression-driven properties. Sliders and color pickers in the UI update the feature state, and the model layer's expressions react to those state changes to:

-   **Animate nodes** — open/close doors, hood, and trunk.
-   **Update materials** — change body color and toggle brake lights.

This pattern is useful when you have multiple model instances in the same source and want to update each independently via its feature ID.

> **Note: iOS Examples App Available**
> 
> This example code is part of the **Maps SDK for iOS Examples App**, a working iOS project [available on Github](https://github.com/mapbox/mapbox-maps-ios/tree/11.29.0/Sources/Examples). iOS developers are encouraged to run the examples app locally to interact with this example in an emulator and explore other features of the Maps SDK.
> 
> See our [Run the Maps SDK for iOS Examples App](https://docs.mapbox.com/help/tutorials/maps-sdk-ios-examples-app/) tutorial for step-by-step instructions.

**Swift**

Title: `Interactive3DModelFeatureStateExample.swift`

[View on GitHub](https://github.com/mapbox/mapbox-maps-ios/blob/main/Sources/Examples/SwiftUI%20Examples/Interactive3DModelFeatureStateExample.swift)

```swift
import MapboxMaps
import SwiftUI
import Combine

/// Showcase interactive 3D model with feature-state driven updates.
/// Demonstrates using expressions and feature state to control model materials and nodes.
struct Interactive3DModelFeatureStateExample: View {
    @StateObject private var viewModel = VehicleViewModel()
    @State private var settingsHeight: CGFloat = 0

    private let sourceId = "3d-model-source"
    private let carModelKey = "car"

    var body: some View {
        MapReader { mapProxy in
            Map(
                initialViewport: .camera(
                    center: CLLocationCoordinate2D(latitude: 40.7155, longitude: -74.0132),
                    zoom: 19.4,
                    bearing: 35,
                    pitch: 60
                )
            ) {
                AmbientLight(id: "environment")
                    .intensity(0.4)

                DirectionalLight(id: "sun_light")
                    .castShadows(true)

                // Add model source with material and node override names
                ModelSource(id: sourceId)
                    .models([createCarModel()])

                // Add model layer with feature-state driven expressions
                ModelLayer(id: "3d-model-layer", source: sourceId)
                    .modelScale(x: 10, y: 10, z: 10)
                    .modelType(.locationIndicator)
                    .modelColor(
                        Exp(.match) {
                            Exp(.get) { "part" }
                            "lights_brakes"
                            Exp(.featureState) { "brake-light-color" }
                            "lights-brakes_reverse"
                            Exp(.featureState) { "brake-light-color" }
                            "lights_brakes_volume"
                            Exp(.featureState) { "brake-light-color" }
                            "lights-brakes_reverse_volume"
                            Exp(.featureState) { "brake-light-color" }
                            Exp(.featureState) { "vehicle-color" }
                        }
                    )
                    .modelColorMixIntensity(
                        Exp(.match) {
                            Exp(.get) { "part" }
                            "body"
                            1.0
                            "lights_brakes"
                            Exp(.featureState) { "brake-light-emission" }
                            "lights-brakes_reverse"
                            Exp(.featureState) { "brake-light-emission" }
                            "lights_brakes_volume"
                            Exp(.featureState) { "brake-light-emission" }
                            "lights-brakes_reverse_volume"
                            Exp(.featureState) { "brake-light-emission" }
                            0.0
                        }
                    )
                    .modelEmissiveStrength(
                        Exp(.match) {
                            Exp(.get) { "part" }
                            "lights_brakes"
                            Exp(.featureState) { "brake-light-emission" }
                            "lights_brakes_volume"
                            Exp(.featureState) { "brake-light-emission" }
                            "lights-brakes_reverse"
                            Exp(.featureState) { "brake-light-emission" }
                            "lights-brakes_reverse_volume"
                            Exp(.featureState) { "brake-light-emission" }
                            0.0
                        }
                    )
                    .modelOpacity(
                        Exp(.match) {
                            Exp(.get) { "part" }
                            "lights_brakes_volume"
                            Exp(.featureState) { "brake-light-emission" }
                            "lights-brakes_reverse_volume"
                            Exp(.featureState) { "brake-light-emission" }
                            1.0
                        }
                    )
                    .modelRotation(
                        Exp(.match) {
                            Exp(.get) { "part" }
                            "doors_front-left"
                            Exp(.featureState) { "doors-front-left" }
                            "doors_front-right"
                            Exp(.featureState) { "doors-front-right" }
                            "hood"
                            Exp(.featureState) { "hood" }
                            "trunk"
                            Exp(.featureState) { "trunk" }
                            [0.0, 0.0, 0.0]
                        }
                    )
            }
            .mapStyle(.standard(show3dObjects: false))
            .additionalSafeAreaInsets(.bottom, settingsHeight)
            .onStyleLoaded { _ in
                updateVehicleColor(mapProxy: mapProxy)
                updateBrakeLights(mapProxy: mapProxy)
                updateLeftDoor(mapProxy: mapProxy)
                updateRightDoor(mapProxy: mapProxy)
                updateHood(mapProxy: mapProxy)
                updateTrunk(mapProxy: mapProxy)
            }
            .ignoresSafeArea()
            .overlay(alignment: .bottom) {
                settingsPanel
                    .onChangeOfSize { settingsHeight = $0.height }
            }
            // Granular feature state updates - each property updates independently
            .onChange(of: viewModel.vehicleColor) { _ in
                updateVehicleColor(mapProxy: mapProxy)
            }
            .onChange(of: viewModel.brakeLights) { _ in
                updateBrakeLights(mapProxy: mapProxy)
            }
            .onChange(of: viewModel.doorsFrontLeft) { _ in
                updateLeftDoor(mapProxy: mapProxy)
            }
            .onChange(of: viewModel.doorsFrontRight) { _ in
                updateRightDoor(mapProxy: mapProxy)
            }
            .onChange(of: viewModel.hood) { _ in
                updateHood(mapProxy: mapProxy)
            }
            .onChange(of: viewModel.trunk) { _ in
                updateTrunk(mapProxy: mapProxy)
            }
        }
    }

    @ViewBuilder
    private var settingsPanel: some View {
        VStack(spacing: 10) {
            Text("Car Controls")
                .font(.headline)

            // Color picker row
            HStack {
                Text("Vehicle color")
                Spacer()
                Image(systemName: "paintpalette")
                ColorPicker("", selection: $viewModel.vehicleColor)
                    .labelsHidden()
            }

            // Trunk
            HStack(spacing: 12) {
                Text("Trunk")
                    .frame(width: 80, alignment: .leading)
                Slider(value: $viewModel.trunk, in: 0...1)
                Image(systemName: "car.side.rear.open")
            }

            // Hood
            HStack(spacing: 12) {
                Text("Hood")
                    .frame(width: 80, alignment: .leading)
                Slider(value: $viewModel.hood, in: 0...1)
                Image(systemName: "car.side.front.open")
            }

            // Front left door
            HStack(spacing: 12) {
                Text("Left door")
                    .frame(width: 80, alignment: .leading)
                Slider(value: $viewModel.doorsFrontLeft, in: 0...1)
                Image(systemName: "car.top.door.front.left.open")
            }

            // Front right door
            HStack(spacing: 12) {
                Text("Right door")
                    .frame(width: 80, alignment: .leading)
                Slider(value: $viewModel.doorsFrontRight, in: 0...1)
                Image(systemName: "car.top.door.front.right.open")
            }

            // Brake lights
            HStack(spacing: 12) {
                Text("Brake lights")
                    .frame(width: 80, alignment: .leading)
                Slider(value: $viewModel.brakeLights, in: 0...1)
                Image(systemName: "exclamationmark.brakesignal")
            }
        }
        .padding()
        .background(.ultraThinMaterial)
        .cornerRadius(12)
        .padding()
    }

    private func createCarModel() -> Model {
        Model(
            id: carModelKey,
            uri: URL(string: "https://docs.mapbox.com/mapbox-gl-js/assets/ego_car.glb")!,
            position: [-74.0132, 40.7155],
            orientation: [0, 0, 0]
        )
        .materialOverrideNames([
            "body",
            "lights_brakes",
            "lights-brakes_reverse",
            "lights_brakes_volume",
            "lights-brakes_reverse_volume"
        ])
        .nodeOverrideNames([
            "doors_front-left",
            "doors_front-right",
            "hood",
            "trunk"
        ])
    }

    private func updateVehicleColor(mapProxy: MapProxy) {
        mapProxy.map?.setFeatureState(
            sourceId: sourceId,
            sourceLayerId: nil,
            featureId: carModelKey,
            state: ["vehicle-color": StyleColor(viewModel.vehicleColor).rawValue]
        ) { _ in }
    }

    private func updateBrakeLights(mapProxy: MapProxy) {
        let brakeColor = StyleColor(UIColor(red: 0.88, green: 0.0, blue: 0.0, alpha: 1.0))

        mapProxy.map?.setFeatureState(
            sourceId: sourceId,
            sourceLayerId: nil,
            featureId: carModelKey,
            state: ["brake-light-color": brakeColor.rawValue, "brake-light-emission": viewModel.brakeLights]
        ) { _ in }
    }

    private func updateLeftDoor(mapProxy: MapProxy) {
        let doorOpeningDegMax = 80.0

        mapProxy.map?.setFeatureState(
            sourceId: sourceId,
            sourceLayerId: nil,
            featureId: carModelKey,
            state: [
                "doors-front-left": [
                    0.0,
                    mix(viewModel.doorsFrontLeft, 0.0, -doorOpeningDegMax),
                    0.0
                ]
            ]
        ) { _ in }
    }

    private func updateRightDoor(mapProxy: MapProxy) {
        let doorOpeningDegMax = 80.0

        mapProxy.map?.setFeatureState(
            sourceId: sourceId,
            sourceLayerId: nil,
            featureId: carModelKey,
            state: [
                "doors-front-right": [
                    0.0,
                    mix(viewModel.doorsFrontRight, 0.0, doorOpeningDegMax),
                    0.0
                ]
            ]
        ) { _ in }
    }

    private func updateHood(mapProxy: MapProxy) {
        mapProxy.map?.setFeatureState(
            sourceId: sourceId,
            sourceLayerId: nil,
            featureId: carModelKey,
            state: [
                "hood": [
                    mix(viewModel.hood, 0.0, 45.0),
                    0.0,
                    0.0
                ]
            ]
        ) { _ in }
    }

    private func updateTrunk(mapProxy: MapProxy) {
        mapProxy.map?.setFeatureState(
            sourceId: sourceId,
            sourceLayerId: nil,
            featureId: carModelKey,
            state: [
                "trunk": [
                    mix(viewModel.trunk, 0.0, -60.0),
                    0.0,
                    0.0
                ]
            ]
        ) { _ in }
    }

    // Helper function to mix values (linear interpolation)
    private func mix(_ t: Double, _ a: Double, _ b: Double) -> Double {
        return b * t - a * (t - 1)
    }
}

private final class VehicleViewModel: ObservableObject {
    @Published var doorsFrontLeft: Double = 0.5
    @Published var doorsFrontRight: Double = 0.0
    @Published var trunk: Double = 0.0
    @Published var hood: Double = 0.0
    @Published var brakeLights: Double = 0.0
    @Published var vehicleColor: Color = .white
}
```