iOS: paint the PHP window background behind tabs chrome - #358
Conversation
`UI.SetBackground` is documented as controlling "what shows behind transparent system bars and safe area insets", and the stack renderer honours it: `StackScreenBackgroundModifier` paints the colour behind each stack screen, extended through the safe areas, because NavigationStack draws its own `systemBackground` container with no SwiftUI override hook. TabView has the same container and no equivalent modifier, so under a tabs root the call is silently a no-op. A themed app gets a system-background band wherever the safe-area-inset screen content cannot reach — behind the nav bar and behind the tab bar — while the identical screen under a stack layout renders correctly. Apply the same modifier in the tabs renderer's level content. The modifier is shared rather than duplicated, so it loses its `private` (file-scoped in Swift) and the stack-specific name: it is now `WindowBackgroundModifier`, still declared in the stack renderer, so no new file and no project.pbxproj change. No-op when no override is set, so the stock appearance is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Thanks for the fix David. Great PR 🙏🏻 One thing I found while checking if this is now consistently applied everywhere; A search tab still misses it. The Are you up to adding those? It'll close the inconsistency everywhere :) Repro, with <top-bar title="Tabs root" />
<bottom-nav label-visibility="labeled">
<bottom-nav-item id="one" icon="home" label="One" url="/tabs-bg" :active="true" />
<bottom-nav-item id="two" icon="person" label="Two" url="/tabs-bg-two" />
</bottom-nav>
<column class="w-full h-full p-6 bg-[#0F172A]">
<text class="text-xl text-white">Tabs root</text>
</column>For the search tab, swap the second item for this one and add <bottom-nav-item id="find" icon="magnifyingglass" label="Find" url="/tabs-search" search :active="true" />public function searchItems(): array
{
return ['Alpha', 'Bravo', 'Charlie'];
}Small one while you're in there: the WindowBackgroundState docblock in UIFunctions.swift lists three surfaces. Mind adding the tabs renderer to it? That comment is the only place this API's behaviour is written down. |
`WindowBackgroundModifier` reached the tab levels but not every surface a tabs root puts on screen. `Tab(role: .search)` renders `SearchTabContainer`, declared as a sibling of the tab `ForEach` (Apple's "if inside TabView" pattern), so it never passes through `PerTabContent` and the search tab stayed light in a themed app. It takes the modifier inside its own NavigationStack — outside it, that stack's container would cover the paint. Its results `List` paints `systemGroupedBackground` over anything behind it, so the scroll background is hidden as well, but only while an override is set, leaving the stock appearance untouched. A level renders `Color.clear` until its tree publishes. Unbackgrounded, that first frame flashes the container through on the first visit to a screen, so the placeholder branches take the background too — in both renderers, since the stack renderer has the same placeholders. Where the `if/else` is content-versus-placeholder the modifier lifts above it; where the populated branch already picks it up further down, only the placeholder carries it, so nothing paints twice. `WindowBackgroundState`'s docblock is the only place this API's behaviour is written down, and now lists the tabs renderer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`WindowBackgroundModifier` passed its expanded color to `background(_:alignment:)`, so the color's `.ignoresSafeArea()` reached the parent's layout. The chrome then measured against a screen the keyboard never shrank: on the search tab — the one place a text field sits inside a screen this modifier backgrounds — typing, clearing the field and leaving the tab left the tab bar latched to a layout that no longer existed, and gone. The stack renderer had the same exposure on any screen with a text field. Move the paint into the background BUILDER. The expanded color stays out of the parent's layout, so the chrome keeps measuring against the real screen while the paint still reaches under the notch, the home indicator and the keyboard. Scoping the ignore to `.container` fixes the latch too, but stops the paint at the keyboard's edge and leaves a black band behind every keyboard. The builder keeps both. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
To be clear, this is Claude's work. I just tested it on the simulator. I lack a deep understanding of the changes. All three done. Search tab. Placeholders. Covered in both renderers — four sites rather than three. Where the Docblock. One more fix, found while verifying the above. Separately, and not this PR: Verified on device (iOS 26 simulator): the search tab takes the window background including behind its results |
UI.SetBackgroundis documented as setting what shows behind transparent system bars and safe-area insets, but under a tabs root it is a silent no-op.NativeRootStackRendererappliesStackScreenBackgroundModifierto every stack-hosted screen, so the PHP-set color fills the safe-area insets there.NativeRootTabsRendererhas no equivalent: TabView hosts its screens on its ownsystemBackgroundcontainer, and screen content is inset below the chrome, so a themed app shows a white band behind the nav bar and behind the tab bar in light mode. There is no app-side workaround — a full-bleed element inside the screenroot cannot reach those strips, and no "ignore safe area" class exists.
This shares the existing modifier rather than adding a second one:
StackScreenBackgroundModifier→WindowBackgroundModifier, no longerprivate, with its doc comment generalized from NavigationStack to both chrome roots.NativeRootTabsRenderer.PerTabContentapplies it alongside.dismissesKeyboardOnTap().No behavior change when no background is set: the modifier returns
contentuntouched, so the stock appearance and iOS 26's adaptive Liquid Glass bars are preserved. Verified on device (iOS 26 simulator and cold launch from a killed process): with the patch the color fills edge to edge behind both bars and the glass survives; without it, the seam returns.