RealityKit Basics: Interaction

Using system gestures to interact with entities in RealityKit.

Overview

We can use standard SwiftUI gestures to interact with 3D content in our scenes. There are a few things we need to keep in mind.

  • Each entity we want to interact with needs to have two components. We can add these in Reality Composer Pro, or directly in code.
    • Input Component
    • Collision Component
  • When we create a gesture, we need to target it to one or more entities. We can use
    • targetedToEntity() to target a known entity
    • targetedToEntity(where:) to target entities based on a query. For example, only run this on entities with a certain component.
    • targetedToAnyEntity() to target any entity in our scene.

Learn more about gestures: SwiftUI Gestures with RealityKit Entities

Examples

Let’s start with a simple tap gesture. When we tap the earth entity we will scale it up slightly. We target the gesture to the earth entity so it won’t fire when we tap on anything else. We use an instance method on Entity to change the transform over time.

.gesture(TapGesture().targetedToEntity(earth)
    .onEnded({ value in
        var newTransform = Transform()
        newTransform.scale = .init(repeating: 1.25)
        earth.move(to: newTransform, relativeTo: earth, duration: 0.3)

    })
)

We can also double-tap the earth to reset the transform back to the original value. Note that we need to place this gesture higher in the call chain than the single tap. Otherwise, we will always fire the single tap.

.gesture(TapGesture(count: 2).targetedToEntity(earth)
    .onEnded({ value in
        earth.move(to: earthTransform, relativeTo: earth.parent!, duration: 0.3)
    })
)

When we tap on the moon, we can trigger a behavior to start a timeline. Behaviors and timelines are defined in Reality Composer Pro. In this case, we have a simple timeline that will spin and orbit the moon. We can trigger that timeline with the On Tap behavior. We’ll call the applyTapForBehaviors() instance method on Entity to trigger the behavior. The order of events is:

  • Tap the moon
  • Call applyTapForBehaviors() to trigger On Tap behavior
  • On Tap behavior calls the timeline
  • The timeline animates the moon over time
.gesture(TapGesture().targetedToEntity(moon)
    .onEnded({ value in
        // call applyTapForBehaviors to trigger the behavior on the moon entity
        if value.entity.applyTapForBehaviors() {
            print("timeline is running")
        }
    })
)

Let’s look at one last example. We’ll use the drag gesture to rotate entities around their Y Axis. This example was adapted from Lab 014 – Building an Indirect Transform System.

fileprivate struct DragToRotate: ViewModifier {

    @State var isDragging: Bool = false
    @State var initialOrientation:simd_quatf = simd_quatf(
        vector: .init(repeating: 0.0)
    )
    @State var rotation: Angle = .zero

    func body(content: Content) -> some View {
        content
            .gesture(
                DragGesture()
                    .targetedToAnyEntity()
                    .onChanged { value in

                        if !isDragging {
                            isDragging = true
                            initialOrientation = value.entity.transform.rotation
                        }
                        rotation.degrees += 0.01 * (value.velocity.width)
                        let rotationTransform = Transform(yaw: Float(rotation.radians))
                        value.entity.transform.rotation = initialOrientation * rotationTransform.rotation
                    }
                    .onEnded { value in
                        isDragging = false
                        initialOrientation = simd_quatf(
                            vector: .init(repeating: 0.0)
                        )
                    }
            )
    }
}

When you’re ready to dive further into gestures, we have an entire series to get you started.

learn visionOS / System Gestures

Video Demo

Full Example Code

struct Example042: View {

    @State var earth: Entity = Entity()
    @State var earthTransform: Transform = Transform()
    @State var moon: Entity = Entity()

    var body: some View {
        RealityView { content in

            // Load a scene from the bundle
            if let scene = try? await Entity(named: "RKBasicsLoading", in: realityKitContentBundle) {
                content.add(scene)
                scene.position.y = -0.4 // Move the entire scene down in the volume

                // search the scene for an entity named "Earth"
                if let earth = scene.findEntity(named: "Earth") {
                    self.earth = earth // capture the entity in state, used to target gestures
                    self.earthTransform = earth.transform // capture transform, used to reset
                }

                if let moon = scene.findEntity(named: "Moon") {
                    self.moon = moon // capture the entity in state, used to target gestures
                }
            }

        }
        // Example 2: Double tap the earth to reset transform
        // Note that we need to place this higher in the call chain than the single tap.
        .gesture(TapGesture(count: 2).targetedToEntity(earth)
            .onEnded({ value in
                earth.move(to: earthTransform, relativeTo: earth.parent!, duration: 0.3)
            })
        )
        // Example 1: Tap the earth to scale it up
        .gesture(TapGesture().targetedToEntity(earth)
            .onEnded({ value in
                var newTransform = Transform()
                newTransform.scale = .init(repeating: 1.25)
                earth.move(to: newTransform, relativeTo: earth, duration: 0.3)

            })
        )
        // Example 3: Tap the moon to fire a timeline from Reality Composer Pro
        .gesture(TapGesture().targetedToEntity(moon)
            .onEnded({ value in
                // call applyTapForBehaviors to trigger the behavior on the moon entity
                if value.entity.applyTapForBehaviors() {
                    print("timeline is running")
                }
            })
        )
        // Example 4, part 2
        .modifier(DragToRotate())
    }
}

// Example 4, part 1
// We'll use a drag gesture to rotate the earth around the Y axis
// Adapted from Lab 014 - Building an Indirect Transform System
// https://stepinto.vision/labs/lab-014-building-an-indirect-transform-system/
fileprivate struct DragToRotate: ViewModifier {

    @State var isDragging: Bool = false
    @State var initialOrientation:simd_quatf = simd_quatf(
        vector: .init(repeating: 0.0)
    )
    @State var rotation: Angle = .zero

    func body(content: Content) -> some View {
        content
            .gesture(
                DragGesture()
                    .targetedToAnyEntity()
                    .onChanged { value in

                        if !isDragging {
                            isDragging = true
                            initialOrientation = value.entity.transform.rotation
                        }
                        rotation.degrees += 0.01 * (value.velocity.width)
                        let rotationTransform = Transform(yaw: Float(rotation.radians))
                        value.entity.transform.rotation = initialOrientation * rotationTransform.rotation
                    }
                    .onEnded { value in
                        isDragging = false
                        initialOrientation = simd_quatf(
                            vector: .init(repeating: 0.0)
                        )
                    }
            )
    }
}

Support our work so we can continue to bring you new examples and articles.

Download the Xcode project with this and many more examples from Step Into Vision.
Some examples are provided as standalone Xcode projects. You can find those here.

Questions or feedback?