ARView ignores multi-touch events

Hi,

How to enable multitouch on ARView?

Touch functions (touchesBegan, touchesMoved, ...) seem to only handle one touch at a time. In order to handle multiple touches at a time with ARView, I have to either:

  • Use SwiftUI .simultaneousGesture on top of an ARView representable
  • Position a UIView on top of ARView to capture touches and do hit testing by passing a reference to ARView

Expected behavior:

  • ARView should capture all touches via touchesBegan/Moved/Ended/Cancelled.

Here is what I tried, on iOS 26.1 and macOS 26.1:

ARView Multitouch

The setup below is a minimal ARView presented by SwiftUI, with touch events handled inside ARView. Multitouch doesn't work with this setup. Note that multitouch wouldn't work either if the ARView is presented with a UIViewController instead of SwiftUI.

import RealityKit
import SwiftUI

struct ARViewMultiTouchView: View {
    var body: some View {
        ZStack {
            ARViewMultiTouchRepresentable()
                .ignoresSafeArea()
        }
    }
}

#Preview {
    ARViewMultiTouchView()
}

// MARK: Representable ARView

struct ARViewMultiTouchRepresentable: UIViewRepresentable {
    
    func makeUIView(context: Context) -> ARView {
        let arView = ARViewMultiTouch(frame: .zero)
        
        let anchor = AnchorEntity()
        arView.scene.addAnchor(anchor)
        
        let boxWidth: Float = 0.4
        let boxMaterial = SimpleMaterial(color: .red, isMetallic: false)
        let box = ModelEntity(mesh: .generateBox(size: boxWidth), materials: [boxMaterial])
        box.name = "Box"
        box.components.set(CollisionComponent(shapes: [.generateBox(width: boxWidth, height: boxWidth, depth: boxWidth)]))
        anchor.addChild(box)
        
        return arView
    }
    
    func updateUIView(_ uiView: ARView, context: Context) { }
}

// MARK: ARView

class ARViewMultiTouch: ARView {
    
    required init(frame: CGRect) {
        super.init(frame: frame)
        
        /// Enable multi-touch
        isMultipleTouchEnabled = true
        
        cameraMode = .nonAR
        automaticallyConfigureSession = false
        environment.background = .color(.gray)
        
        /// Disable gesture recognizers to not conflict with touch events
        /// But it doesn't fix the issue
        gestureRecognizers?.forEach { $0.isEnabled = false }
    }
    
    required dynamic init?(coder decoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        for touch in touches {
            /// # Problem
            /// This should print for every new touch, up to 5 simultaneously on an iPhone (multi-touch)
            /// But it only fires for one touch at a time (single-touch)
            print("Touch began at: \(touch.location(in: self))")
        }
    }

}

Multitouch with an Overlay

This setup works, but it doesn't seem right. There must be a solution to make ARView handle multi touch directly, right?

import SwiftUI
import RealityKit

struct MultiTouchOverlayView: View {
    var body: some View {
        ZStack {
            MultiTouchOverlayRepresentable()
                .ignoresSafeArea()
            Text("Multi touch with overlay view")
                .font(.system(size: 24, weight: .medium))
                .foregroundStyle(.white)
                .offset(CGSize(width: 0, height: -150))
        }
    }
}

#Preview {
    MultiTouchOverlayView()
}

// MARK: Representable Container

struct MultiTouchOverlayRepresentable: UIViewRepresentable {
    
    func makeUIView(context: Context) -> UIView {
        /// The view that SwiftUI will present
        let container = UIView()
        
        /// ARView
        let arView = ARView(frame: container.bounds)
        arView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        arView.cameraMode = .nonAR
        arView.automaticallyConfigureSession = false
        arView.environment.background = .color(.gray)
        
        let anchor = AnchorEntity()
        arView.scene.addAnchor(anchor)
        
        let boxWidth: Float = 0.4
        let boxMaterial = SimpleMaterial(color: .red, isMetallic: false)
        let box = ModelEntity(mesh: .generateBox(size: boxWidth), materials: [boxMaterial])
        box.name = "Box"
        box.components.set(CollisionComponent(shapes: [.generateBox(width: boxWidth, height: boxWidth, depth: boxWidth)]))
        anchor.addChild(box)
        
        /// The view that will capture touches
        let touchOverlay = TouchOverlayView(frame: container.bounds)
        touchOverlay.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        touchOverlay.backgroundColor = .clear
        
        /// Pass an arView reference to the overlay for hit testing
        touchOverlay.arView = arView
        
        /// Add views to the container.
        /// ARView goes in first, at the bottom.
        container.addSubview(arView)
        /// TouchOverlay goes in last, on top.
        container.addSubview(touchOverlay)
        
        return container
    }
    
    func updateUIView(_ uiView: UIView, context: Context) {
        
    }
}

// MARK: Touch Overlay View

/// A UIView to handle multi-touch on top of ARView
class TouchOverlayView: UIView {
    
    weak var arView: ARView?
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        isMultipleTouchEnabled = true
        isUserInteractionEnabled = true
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        let totalTouches = event?.allTouches?.count ?? touches.count
        print("--- Touches Began --- (New: \(touches.count), Total: \(totalTouches))")
        
        for touch in touches {
            let location = touch.location(in: self)
            
            /// Hit testing.
            /// ARView and Touch View must be of the same size
            if let arView = arView {
                let entity = arView.entity(at: location)
                if let entity = entity {
                    print("Touched entity: \(entity.name)")
                } else {
                    print("Touched: none")
                }
            }
        }
    }
    
    override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
        let totalTouches = event?.allTouches?.count ?? touches.count
        print("--- Touches Cancelled --- (Cancelled: \(touches.count), Total: \(totalTouches))")
    }
}
ARView ignores multi-touch events
 
 
Q