We render a grid of small USDZ models in SceneKit, one model per cell, and each model should spin continuously around its own local Y axis (one full turn every ~8 seconds). Simple grids of 7 and 12 cells work. A full 7×7 (49) or 12×12 (144) does not.
Today each spinning cell is its own SCNView with rendersContinuously = true and an SCNAction.rotateBy on that cell’s pivot node. On macOS, opening a 7×7 of these views deadlocks SceneKit: layout stalls inside setFrameSize / setBackgroundColor while every render thread waits on the shared irradiance lock. We cap live views at 12 and keep the remaining cells as static bitmap snapshots, mounted in waves of four. Frame rate is already tiered (12 / 24 / 30 fps) and antialiasing is reduced on weaker devices. The same grid runs on macOS, iPad, and iPhone.
What is the efficient way to spin every cell of a 7×7 and a 12×12 around its own axis? Specifically: one shared SCNScene (or one MTKView plus SCNRenderer) with many nodes, each rotating locally, instead of one SCNView per cell — and how should we share geometry, avoid per-view render loops, and stay off that irradiance lock during layout?
One scene with many nodes, as you suggest, is the right approach. The SCNNode documentation describes this pattern for a racing game with many identical cars. Each car gets its own node to position and animate it, and every car node references the same geometry object.
The clone() method gives you that sharing. Its documentation says cloning lets you "place the same geometry at several locations within a scene." So you can load each USDZ once and place a clone in every cell that shows it. Each clone can have its own pivot node that runs a repeating rotation. The clones share one copy of the geometry and its materials, so changing one clone's material changes every cell with that model. The clone() documentation shows how to copy the geometry first when a cell needs its own material.
Flattening the whole grid with flattenedClone() would stop the cells from spinning independently. Its documentation says it's for part of a node hierarchy that will "remain static" with respect to itself. Each model spins as a unit, though. The WWDC17 session SceneKit in Swift Playgrounds recommends flattening anything that doesn't move relative to something else. If a model loads as several nodes, you can flatten it once and clone the flattened node into each cell. Setting showsStatistics to true makes the view display rendering statistics, which the session uses to compare draw calls before and after flattening.
One SCNView can show the whole grid. SCNRenderer is for adding SceneKit content to an app that already renders with Metal directly, so you don't need an MTKView for this grid. Spinning the cells doesn't require rendersContinuously either. The documentation says that with the default of false, the view redraws only when something in the scene graph changes or animates.
I tested the default in macOS 27.0.1 with one view showing 49 clones, each under a pivot running a repeating rotateBy action. All 49 clones shared the original's geometry object. The view called its delegate's renderer(_:updateAtTime:) method 60 times per second, matching the 60 Hz display, for the full 5-second run. I also resized the view and changed its background color up to 60 times per second while it rendered. With 49 or 144 clones, a lighting environment, 30 frames per second, and 4x multisampling, the main thread never waited more than 0.05 seconds.
SceneKit is now deprecated. The SceneKit documentation says to "use RealityKit instead" and points to the WWDC25 session Bring your SceneKit project to RealityKit. For a grid that has to keep working on Mac, iPad, and iPhone, the deprecation is worth considering as you restructure.
Separately, in macOS 27.0.1 I saw a similar hang with 49 continuously rendering views when each view's scene had its own lighting environment image. The main thread stayed blocked until I ended the app, after more than a minute. The hang didn't happen with 36 views, or with no lighting environment. When all 49 views shared one image, the app froze for up to 5.5 seconds while they opened, then recovered. The deadlock you're seeing is worth a bug report. Please file one with a small project that reproduces it and a spindump captured while the app is hung, and post the Feedback number here. Bug Reporting: How and Why? has tips on creating your report.