Skip to content

[Feature] Add UntoldViewOptions for runtime view settings in SwiftUI - #1131

Merged
untoldengine merged 1 commit into
untoldengine:developfrom
miolabs:feature/untoldview_options
Aug 11, 2026
Merged

[Feature] Add UntoldViewOptions for runtime view settings in SwiftUI#1131
untoldengine merged 1 commit into
untoldengine:developfrom
miolabs:feature/untoldview_options

Conversation

@miogds

@miogds miogds commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Implements the UntoldViewOptions split we discussed: UntoldRendererConfig stays immutable/create-time, and runtime view-host settings (target FPS being the motivating case) get their own small Equatable struct that is diff-applied to the live MTKView — no renderer recreation, no Observable conformance on the renderer. The full design rationale is included in this PR at docs/proposals/UntoldViewOptions.md.

What's in here

UntoldViewOptionsEquatable, Sendable struct with preferredFramesPerSecond, isPaused, clearColor. Usable two ways:

UntoldView(renderer: renderer, options: UntoldViewOptions(preferredFramesPerSecond: 30)) { ... }

// or modifier style (same copy-on-write pattern as .onUpdate):
UntoldView(renderer: renderer) { ... }
    .preferredFramesPerSecond(fps)
    .paused(inMenu)

When a bound SwiftUI value changes, the body re-evaluates, updateNSView/updateUIView runs, and the coordinator applies only the properties that differ from the last-applied options. Unrelated re-evaluations cost one Equatable compare and never touch the view.

Stable renderer ownership — the "renderer lives in a stable reference holder" part: SceneView's Coordinator now owns the renderer. Previously both UntoldView.init and SceneView.init ran renderer ?? UntoldRenderer.create() on every SwiftUI re-evaluation of the view struct. Now the injected renderer is adopted (or a fallback created) exactly once, on first makeNSView/makeUIView, and never swapped.

Scene content built onceUntoldView stores the @SceneBuilder closure and runs it a single time when the platform view is created, after the renderer exists (so setEntityMeshAsync has a Metal device). Previously the builder re-ran on every re-evaluation — in the SceneBuilder demo that meant new entities and mesh reloads on every timer tick.

SceneView.onInit semantic change (intentional, please review) — the block used to execute immediately at body-evaluation time, on every re-evaluation, and required the renderer to already exist. It now runs exactly once, when the platform view is created and the renderer is ready. The block is also @MainActor now. This makes SceneView().onInit { ... } with a nil renderer safe.

Scope rules going forward

Every property in UntoldViewOptions must be applicable to the live MTKView. Pipeline-affecting settings stay in UntoldRendererConfig; engine tunables (AA, post-FX, LOD, ...) stay in the engine settings API — no duplication, one source of truth each.

Not included (follow-ups per the proposal)

  • renderScale (needs care: create() pins contentsScale = 1.0)
  • drawsOnDemand (enableSetNeedsDisplay mode)

Testing

  • swift build clean for the engine, all demos, and Sandbox — all existing SceneView(renderer:) / UntoldView(renderer:) call sites compile unchanged (new parameters are defaulted).
  • New UntoldViewOptionsTests (7 tests, no Metal device needed): struct semantics, modifier copy-on-write, first-apply sets everything, diff-apply leaves unchanged properties untouched, equal options are a no-op. All pass, plus existing SceneBuilderNodeTests.

@miogds
miogds requested a review from untoldengine as a code owner August 5, 2026 22:05
Adds a runtime-tunable options struct for the SwiftUI host view, per the
UntoldViewOptions proposal (docs/proposals/UntoldViewOptions.md):

- UntoldViewOptions (Equatable, Sendable): preferredFramesPerSecond,
  isPaused, clearColor. Settable via UntoldView(renderer:options:) or the
  .preferredFramesPerSecond(_:)/.paused(_:)/.options(_:) modifiers.
- SceneView's Coordinator now owns the renderer: a fallback renderer is
  created exactly once instead of on every SwiftUI body re-evaluation,
  and options are diffed against the last-applied copy so only changed
  properties touch the live MTKView. The renderer is never recreated.
- UntoldView builds its scene content once, when the platform view is
  created (after the renderer exists, so mesh loading has a Metal
  device), instead of on every re-evaluation of the view struct.
- SceneView.onInit is deferred accordingly: the block now runs once when
  the platform view is created rather than immediately at body-eval time
  on every re-evaluation.

UntoldRendererConfig stays immutable, create-time only.
@miogds
miogds force-pushed the feature/untoldview_options branch from e83234a to 9ce94c7 Compare August 6, 2026 17:53
@untoldengine
untoldengine merged commit 1720ba4 into untoldengine:develop Aug 11, 2026
6 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants