Hello,
I have noticed a performance drop on SpriteKit-based projects running on iOS 26.0 (23A341).
Below is a SpriteKit scene used to test framerate on different devices:
import SpriteKit
import SwiftUI
class BareboneScene: SKScene {
override func didMove(to view: SKView) {
size = view.bounds.size
anchorPoint = CGPoint(x: 0.5, y: 0.5)
backgroundColor = .darkGray
let roundedSquare = SKShapeNode(rectOf: CGSize(width: 150, height: 75), cornerRadius: 12)
roundedSquare.fillColor = .systemRed
roundedSquare.strokeColor = .black
roundedSquare.lineWidth = 3
addChild(roundedSquare)
let action = SKAction.rotate(byAngle: .pi, duration: 1)
roundedSquare.run(.repeatForever(action))
}
}
struct BareboneSceneView: View {
var body: some View {
SpriteView(
scene: BareboneScene(),
debugOptions: [.showsFPS]
)
.ignoresSafeArea()
}
}
#Preview {
BareboneSceneView()
}
The scene is very simple, yet framerate drops to ~40 fps as shown by the Metal HUD. Tested on:
iPhone 13, iOS 26.0: framerate drops to 40 fps. Sometimes it runs at near 60fps. But if the screen is touched repeatedly, the framerate drops to 40-50 fps again.
iPhone 11 Pro, iOS 26.0: ~40fps.
iPad 9th Gen, iOS 18.6.2: 60fps, no issues.
See screenshots attached. These numbers were observed by me and members of our beloved SpriteKit Discord server.
Thank you for your attention.
Selecting any option will automatically load the page
Post
Replies
Boosts
Views
Activity
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))")
}
}
Hi,
Toggling a SwiftUI menu in iOS 26 significantly reduces the framerate of an underlying SKView or ARView.
Below are test cases for SpriteKit and RealityKit. I ran these tests on iOS 26.1 Beta using an iPhone 13 (A15 chip). Results were similar on iOS 26.0.1.
Both scenes consist of circles and balls bouncing on the ground. The restitution of the physics bodies is set for near-perfect elasticity, so they keep bouncing indefinitely.
In both SKView and ARView, the framerate drops significantly whenever the SwiftUI menu is toggled. The menu itself is simple and uses standard SwiftUI animations and styling.
SpriteKit
import SpriteKit
import SwiftUI
class SKRestitutionScene: SKScene {
override func didMove(to view: SKView) {
view.contentMode = .center
size = view.bounds.size
scaleMode = .resizeFill
backgroundColor = .darkGray
anchorPoint = CGPoint(x: 0.5, y: 0.5)
let groundWidth: CGFloat = 300
let ground = SKSpriteNode(color: .gray, size: CGSize(width: groundWidth, height: 10))
ground.physicsBody = SKPhysicsBody(rectangleOf: ground.size)
ground.physicsBody?.isDynamic = false
addChild(ground)
let circleCount = 5
let spacing: CGFloat = 60
let totalWidth = CGFloat(circleCount - 1) * spacing
let startX = -totalWidth / 2
for i in 0..<circleCount {
let circle = SKShapeNode(circleOfRadius: 18)
circle.fillColor = .systemOrange
circle.lineWidth = 0
circle.physicsBody = SKPhysicsBody(circleOfRadius: 18)
circle.physicsBody?.restitution = 1
circle.physicsBody?.linearDamping = 0
let x = startX + CGFloat(i) * spacing
circle.position = CGPoint(x: x, y: 150)
addChild(circle)
}
}
override func willMove(from view: SKView) {
self.removeAllChildren()
}
}
struct SKRestitutionView: View {
var body: some View {
ZStack {
SpriteView(scene: SKRestitutionScene(), preferredFramesPerSecond: 120)
.ignoresSafeArea()
VStack {
Spacer()
Menu {
Button("Edit", systemImage: "pencil") {}
Button("Share", systemImage: "square.and.arrow.up") {}
Button("Delete", systemImage: "trash") {}
} label: {
Text("Menu")
}
.buttonStyle(.glass)
}
.padding()
}
}
}
#Preview {
SKRestitutionView()
}
RealityKit
import RealityKit
import SwiftUI
struct ARViewPhysicsRestitution: UIViewRepresentable {
let arView = ARView()
func makeUIView(context: Context) -> some ARView {
arView.contentMode = .center
arView.cameraMode = .nonAR
arView.automaticallyConfigureSession = false
arView.environment.background = .color(.gray)
// MARK: Root
let anchor = AnchorEntity()
arView.scene.addAnchor(anchor)
// MARK: Camera
let camera = Entity()
camera.components.set(PerspectiveCameraComponent())
camera.position = [0, 1, 4]
camera.look(at: .zero, from: camera.position, relativeTo: nil)
anchor.addChild(camera)
// MARK: Ground
let groundWidth: Float = 3.0
let ground = Entity()
let groundMesh = MeshResource.generateBox(width: groundWidth, height: 0.1, depth: groundWidth)
let groundModel = ModelComponent(mesh: groundMesh, materials: [SimpleMaterial(color: .white, roughness: 1, isMetallic: false)])
ground.components.set(groundModel)
let groundShape = ShapeResource.generateBox(width: groundWidth, height: 0.1, depth: groundWidth)
let groundCollision = CollisionComponent(shapes: [groundShape])
ground.components.set(groundCollision)
let groundPhysicsBody = PhysicsBodyComponent(
material: PhysicsMaterialResource.generate(friction: 0, restitution: 0.97),
mode: .static
)
ground.components.set(groundPhysicsBody)
anchor.addChild(ground)
// MARK: Balls
let ballCount = 5
let spacing: Float = 0.4
let totalWidth = Float(ballCount - 1) * spacing
let startX = -totalWidth / 2
let radius: Float = 0.12
let ballMesh = MeshResource.generateSphere(radius: radius)
let ballMaterial = SimpleMaterial(color: .systemOrange, roughness: 1, isMetallic: false)
let ballShape = ShapeResource.generateSphere(radius: radius)
for i in 0..<ballCount {
let ball = Entity()
let ballModel = ModelComponent(mesh: ballMesh, materials: [ballMaterial])
ball.components.set(ballModel)
let ballCollision = CollisionComponent(shapes: [ballShape])
ball.components.set(ballCollision)
var ballPhysicsBody = PhysicsBodyComponent(
material: PhysicsMaterialResource.generate(friction: 0, restitution: 0.97), /// 0.97 for near perfect elasticity
mode: .dynamic
)
ballPhysicsBody.linearDamping = 0
ballPhysicsBody.angularDamping = 0
ball.components.set(ballPhysicsBody)
let shadow = GroundingShadowComponent(castsShadow: true)
ball.components.set(shadow)
let x = startX + Float(i) * spacing
ball.position = [x, 1, 0]
anchor.addChild(ball)
}
return arView
}
func updateUIView(_ uiView: UIViewType, context: Context) {
}
}
struct PhysicsRestitutionView: View {
var body: some View {
ZStack {
ARViewPhysicsRestitution()
.ignoresSafeArea()
.background(.black)
VStack {
Spacer()
Menu {
Button("Edit", systemImage: "pencil") {}
Button("Share", systemImage: "square.and.arrow.up") {}
Button("Delete", systemImage: "trash") {}
} label: {
Text("Menu")
}
.buttonStyle(.glass)
}
.padding()
}
}
}
#Preview {
PhysicsRestitutionView()
}
Hi,
I can't see RealityKit statistics on Xcode Canvas using:
arView.debugOptions = [.showStatistics]
The statistics only show on a physical device, not Xcode live canvas with #Preview. Testing in Xcode 26.0.1 (17A400) on Tahoe 26.0.1 (25A362).
Use case: I'm using RealityKit as a non-AR 3D engine. Xcode Canvas is useful for live iterations.
Is this expected behavior? How can I see FPS on Xcode canvas? SKView for example shows all debug options on both Xcode Canvas and physical devices.
Topic:
Graphics & Games
SubTopic:
RealityKit
Hi!
Using ARView in UIKit or through a UIViewRepresentable in SwiftUI, we can do:
arView.debugOptions = [.showPhysics, .showStatistics]
What is the equivalent in RealityView?
Topic:
Graphics & Games
SubTopic:
RealityKit