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.

The timeline

A destination mounted by a NavHost goes through these moments, in this order:

MomentWhen it firesUse it forDo not
AwakeOn spawn. NavHost pre-spawns every destination in the graph, hidden, at bootCache components, register non-UI callbacksQuery UXML elements, assume UIView exists, or run per-visit logic: hidden destinations spawn long before the player opens them
OnUILoaded(root) / WhenUILoadedThe first PanelRenderer delivery once the view root resolvesElement queries, Fade event subscriptions, one-time wiringTreat it as a per-visit data refresh: it fires once, not per push
Born visibilitySynchronously inside that same reload, before the first paintNothing: automatic. Start Visible nodes show, everything else hidesSynthesize 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 itReset transient view state so the screen opens freshAssume it fires on the first push, or use it as the args-consume hook
Fade.OnFadeInStartEvery fade-in, including the firstThe per-visit refresh: read GetArgs<T>(), update labels, repopulate lists. See Typed DestinationsOne-time wiring
Fade.OnFadeInFade-in completeFocus side effects, analytics, entrance juiceRequired data binding: too late, the player already saw the screen
Fade.OnFadeOutStart / Fade.OnFadeOutClosingTear down per-visit stateTreat 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 treeKeep references to old VisualElements
Render occlusionSetRenderOccluded from Overlay Opaque or tab switchesIUIViewRenderAware.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

Where the fade hooks live

There are two subscription surfaces, and mixing them up is a common source of confusion:

  • Component-level code (your 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.
  • Custom 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:

csharp
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);

Debugging a lifecycle problem

  • Tick the per-view Debug checkbox on UIViewComponent: it logs the resolved PanelRenderer chain, every fade start/complete, and enable/disable transitions.
  • In play mode, select the destination's node in Tools > CupkekGames > Graphs > Runtime Debugger: the Selected pane shows loaded/visible/occluded state, the resolved root, panel ownership, and the current args type.
  • The classic symptoms map: null queries mean code ran before 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).

See also

Settings

Theme

Light

Contrast

Material

Dark

Dim

Material Dark

System

Sidebar(Light & Contrast only)

Light
Dark

Font Family

DM Sans

Wix

Inclusive Sans

AR One Sans

Direction

LTR
RTL