SpriteKit/RealityKit + SwiftUI Performance Regression on iOS 26

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()
}

I filed report FB20808104 with the above information and code.

Expected behavior: a native UI menu shouldn't impact the performance of underlying views, or use significant system resources.

SpriteKit/RealityKit &#43; SwiftUI Performance Regression on iOS 26
 
 
Q