Created
August 22, 2026 20:09
-
-
Save lanserxt/cc22ac5dd0d8cea2bbd877d1ce18fd48 to your computer and use it in GitHub Desktop.
iOS 27: UISDKit Framework example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // | |
| // USDInspectorModel.swift | |
| // WWDC25Demo | |
| // | |
| // Created by Anton Gubarenko on 22.08.2026. | |
| // | |
| import SwiftUI | |
| import Observation | |
| import RealityKit | |
| import USDKit | |
| @available(iOS 27.0, *) | |
| @MainActor | |
| @Observable | |
| final class USDInspectorModel { | |
| struct PrimRow: Identifiable, Hashable { | |
| let id: String | |
| let path: String | |
| let description: String | |
| let specifier: String | |
| let propertyCount: Int | |
| let childCount: Int | |
| } | |
| private(set) var stage: USDStage? | |
| private(set) var prims: [PrimRow] = [] | |
| private(set) var errorMessage: String? | |
| private(set) var isLoaded = false | |
| let source = """ | |
| #usda 1.0 | |
| ( | |
| defaultPrim = "Showroom" | |
| metersPerUnit = 1 | |
| upAxis = "Y" | |
| ) | |
| def Xform "Showroom" | |
| { | |
| def Xform "Table" | |
| { | |
| def Cube "Product" | |
| { | |
| double size = 0.4 | |
| } | |
| } | |
| def Xform "LightRig" | |
| { | |
| def Sphere "KeyLight" | |
| { | |
| double radius = 0.1 | |
| } | |
| } | |
| } | |
| """ | |
| func load() { | |
| do { | |
| let stage = try USDStage(string: source, loadingPayloads: .all) | |
| self.stage = stage | |
| self.prims = stage.descendants.map { | |
| PrimRow( | |
| id: String(describing: $0.path), | |
| path: String(describing: $0.path), | |
| description: $0.description, | |
| specifier: String(describing: $0.specifier), | |
| propertyCount: $0.properties.count, | |
| childCount: $0.children.count | |
| ) | |
| } | |
| self.errorMessage = nil | |
| self.isLoaded = true | |
| } catch { | |
| self.stage = nil | |
| self.prims = [] | |
| self.errorMessage = error.localizedDescription | |
| self.isLoaded = false | |
| } | |
| } | |
| func reload() { | |
| stage = nil | |
| prims = [] | |
| isLoaded = false | |
| load() | |
| } | |
| } | |
| @available(iOS 27.0, *) | |
| struct USDStagePreview: View { | |
| let stage: USDStage | |
| @State private var rootEntity: Entity? | |
| @State private var pitchDegrees = 0.0 | |
| @State private var yawDegrees = 0.0 | |
| @State private var rollDegrees = 0.0 | |
| var body: some View { | |
| VStack(spacing: 12) { | |
| RealityView { content in | |
| let root = Entity() | |
| root.components.set( | |
| USDStageComponent(allowsHitTesting: true) | |
| ) | |
| content.add(root) | |
| rootEntity = root | |
| _ = await USDStageComponent.render( | |
| stage, | |
| to: root, | |
| at: .default | |
| ) | |
| applyRotation(to: root) | |
| } update: { _ in | |
| if let rootEntity { | |
| applyRotation(to: rootEntity) | |
| } | |
| } | |
| .frame(minHeight: 280) | |
| .clipShape(RoundedRectangle(cornerRadius: 16)) | |
| VStack(alignment: .leading, spacing: 10) { | |
| rotationSlider( | |
| title: "Pitch", | |
| value: $pitchDegrees, | |
| systemImage: "rotate.3d" | |
| ) | |
| rotationSlider( | |
| title: "Yaw", | |
| value: $yawDegrees, | |
| systemImage: "rotate.right" | |
| ) | |
| rotationSlider( | |
| title: "Roll", | |
| value: $rollDegrees, | |
| systemImage: "arrow.triangle.2.circlepath" | |
| ) | |
| } | |
| .padding(.horizontal) | |
| } | |
| } | |
| private func rotationSlider( | |
| title: String, | |
| value: Binding<Double>, | |
| systemImage: String | |
| ) -> some View { | |
| VStack(alignment: .leading, spacing: 4) { | |
| HStack { | |
| Label(title, systemImage: systemImage) | |
| Spacer() | |
| Text("\(Int(value.wrappedValue))°") | |
| .monospacedDigit() | |
| .foregroundStyle(.secondary) | |
| } | |
| Slider(value: value, in: -180...180) | |
| } | |
| } | |
| private func applyRotation(to root: Entity) { | |
| let pitch = Float(pitchDegrees * .pi / 180) | |
| let yaw = Float(yawDegrees * .pi / 180) | |
| let roll = Float(rollDegrees * .pi / 180) | |
| root.transform = Transform(pitch: pitch, yaw: yaw, roll: roll) | |
| } | |
| } | |
| @available(iOS 27.0, *) | |
| struct USDInspectorView: View { | |
| @State private var model = USDInspectorModel() | |
| @State private var selectedTab = 0 | |
| var body: some View { | |
| NavigationStack { | |
| Group { | |
| if let errorMessage = model.errorMessage { | |
| ContentUnavailableView( | |
| "Couldn't Load USD", | |
| systemImage: "cube.transparent", | |
| description: Text(errorMessage) | |
| ) | |
| } else if let stage = model.stage { | |
| VStack(spacing: 0) { | |
| Picker("View", selection: $selectedTab) { | |
| Text("Scene").tag(0) | |
| Text("Hierarchy").tag(1) | |
| Text("Source").tag(2) | |
| } | |
| .pickerStyle(.segmented) | |
| .padding() | |
| switch selectedTab { | |
| case 0: | |
| USDStagePreview(stage: stage) | |
| case 1: | |
| List(model.prims) { prim in | |
| VStack(alignment: .leading, spacing: 6) { | |
| Text(prim.path).font(.headline) | |
| Text(prim.description) | |
| .font(.caption) | |
| .foregroundStyle(.secondary) | |
| .lineLimit(2) | |
| HStack(spacing: 12) { | |
| Label("\(prim.childCount)", systemImage: "arrow.triangle.branch") | |
| Label("\(prim.propertyCount)", systemImage: "list.bullet.rectangle") | |
| Text(prim.specifier).font(.caption.monospaced()) | |
| } | |
| .foregroundStyle(.secondary) | |
| } | |
| .padding(.vertical, 4) | |
| } | |
| default: | |
| ScrollView { | |
| Text(model.source) | |
| .font(.system(.caption, design: .monospaced)) | |
| .frame(maxWidth: .infinity, alignment: .leading) | |
| .textSelection(.enabled) | |
| .padding() | |
| } | |
| } | |
| } | |
| } else { | |
| ProgressView("Loading USD stage…") | |
| } | |
| } | |
| .navigationTitle("USD Inspector") | |
| .toolbar { | |
| ToolbarItem(placement: .topBarTrailing) { | |
| Button { | |
| model.reload() | |
| } label: { | |
| Image(systemName: "arrow.clockwise") | |
| } | |
| } | |
| } | |
| .task { | |
| if !model.isLoaded { | |
| model.load() | |
| } | |
| } | |
| } | |
| } | |
| } | |
| struct USDKitArticleDemoView: View { | |
| var body: some View { | |
| if #available(iOS 27.0, *) { | |
| USDInspectorView() | |
| } else { | |
| ContentUnavailableView( | |
| "iOS 27 Required", | |
| systemImage: "cube.transparent", | |
| description: Text("This sample uses the USDKit framework.") | |
| ) | |
| } | |
| } | |
| } | |
| #Preview { | |
| USDKitArticleDemoView() | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment