Every rule on this page exists elsewhere in the docs. This page puts them in one ordered timeline, because most first-project bugs are lifecycle bugs: querying too early, binding data at the wrong moment, or treating a render state as a logical one.
A destination mounted by a NavHost goes through these moments, in this order:
| Moment | When it fires | Use it for | Do not |
|---|---|---|---|
Awake | On spawn. NavHost pre-spawns every destination in the graph, hidden, at boot | Cache components, register non-UI callbacks | Query UXML elements, assume UIView exists, or run per-visit logic: hidden destinations spawn long before the player opens them |
OnUILoaded(root) / WhenUILoaded | The first PanelRenderer delivery once the view root resolves | Element queries, Fade event subscriptions, one-time wiring | Treat it as a per-visit data refresh: it fires once, not per push. Spawn, destroy, or toggle another PanelRenderer synchronously: the hook runs inside Unity's panel update, so defer that with DeferOneTick (see below) |
| Born visibility | Synchronously inside that same reload, before the first paint | Nothing: automatic. Start Visible nodes show, everything else hides | Synthesize show/fade callbacks for born-visible views. The host's boot seeding push already runs one real fade cycle; firing your own double-counts |
OnStateReset(args) | Before a reopen push (the 2nd and later), when the node sets Reset State On Reopen. The first-ever push does not fire it | Reset transient view state so the screen opens fresh | Assume it fires on the first push, or use it as the args-consume hook |
Fade.OnFadeInStart | Every fade-in, including the first | The per-visit refresh: read GetArgs<T>(), update labels, repopulate lists. See Typed Destinations | One-time wiring |
Fade.OnFadeIn | Fade-in complete | Focus side effects, analytics, entrance juice | Required data binding: too late, the player already saw the screen |
Fade.OnFadeOutStart / Fade.OnFadeOut | Closing | Tear down per-visit state | Treat a render occlusion as a close |
OnViewRootChanged(root) | A later reload whose resolved root differs (UXML live reload, enable cycles) | Re-query and re-attach: everything wired in OnUILoaded died with the old tree | Keep references to old VisualElements |
| Render occlusion | SetRenderOccluded from Overlay Opaque or tab switches | IUIViewRenderAware.OnRenderShown / OnRenderHidden on registered UI Actions, for controls that need repair after display: none (Luna's GridView does this) | Treat it as a logical close. The view stays open on its stack the whole time; see Occlusion |
There are two subscription surfaces, and mixing them up is a common source of confusion:
UIViewComponent subclass, or any external controller) subscribes to the events on component.Fade: OnFadeInStart, OnFadeIn, OnFadeOutStart, OnFadeOut. This is the pattern every built-in Luna controller uses.UIView subclasses override the protected virtuals of the same names on UIView. You rarely need this.Because OnUILoaded can run again after a live reload (via OnViewRootChanged re-wiring), always unsubscribe before subscribing:
protected override void OnUILoaded(VisualElement root)
{
_title = root.Q<Label>("title");
Fade.OnFadeInStart -= Refresh;
Fade.OnFadeInStart += Refresh;
}
protected override void OnViewRootChanged(VisualElement root) => OnUILoaded(root);OnUILoaded, OnViewRootChanged, UILoaded, and the deferred branch of WhenUILoaded are all delivered from PanelRenderer's reload callback, and Unity invokes that from its runtime panel update while iterating the live panels. Anything that adds or removes a panel in that moment (instantiating a prefab with a PanelRenderer, destroying one, SetActive on its GameObject) corrupts the iteration. The exception is delayed and misattributed: InvalidOperationException: Collection was modified on the next update, thrown from Unity's loop, with your hook absent from the stack.
Views defer such spawns with DeferOneTick(action); non-view code uses LunaUIManager.Instance.DeferOneTick(action). Both run the action next frame, outside the pass. Ordinary tree work (queries, callbacks, classes, styles, Fade subscriptions) is fine inside the hook. Details and the code shape are on UIViewComponent; PanelRendererBinder's OnPanelReady has the same contract.
UIViewComponent: it logs the resolved PanelRenderer chain, every fade start/complete, and enable/disable transitions.OnUILoaded; data that appears only on the second visit means args were read in OnUILoaded instead of Fade.OnFadeInStart; a screen that silently never appears usually means an unresolvable Parent Name (the console now logs this as an error); a Collection was modified exception from Unity's panel update with none of your code on the stack means a load hook spawned or destroyed a panel synchronously (defer it with DeferOneTick).Settings
Theme
Light
Contrast
Material
Dark
Dim
Material Dark
System
Sidebar(Light & Contrast only)
Font Family
DM Sans
Wix
Inclusive Sans
AR One Sans
Direction