From 85f5fb2980891a34c782bd858f89ecc5632bbad7 Mon Sep 17 00:00:00 2001 From: Ryanmello07 <67509637+Ryanmello07@users.noreply.github.com> Date: Tue, 8 Sep 2026 19:42:54 -0700 Subject: [PATCH] Draw the Pro celebration in a clear overlay, not over the tab view On a current toolchain the app renders as nothing but SwiftUI's unsupported-view placeholder -- a full-screen yellow field with a red slashed circle -- while the process runs normally underneath: the tunnel connects, the rpc flows, the connect grid updates. Only the UI is gone. The pixelation is a raster-layer filter. SwiftUI has to render the filtered content into an offscreen layer for the Metal function to sample, and content backed by UIKit cannot go into one; when it cannot, SwiftUI substitutes the placeholder for the whole filtered region. At MainView the filtered content is the entire app, including the UIKit-backed tab view, so the placeholder is the entire app. `isEnabled: false` does not avoid this. The effect being ATTACHED is what asks for the raster layer, and ProPixelation attaches it unconditionally -- deliberately, to keep the tab view from being re-hosted above the overlay when the structure changes. That trade is still the right one; it is only the choice of what to wrap that was wrong. So wrap nothing. The layer goes into a clear overlay in the same chain position, which keeps the confetti -- plain SwiftUI, and the part the user actually sees -- and costs only the mosaic over the root, which on this toolchain was never renderable there anyway. The environment object is still injected below the overlay, and the overlay is proposed the same rect the layer got before, so the confetti geometry is unchanged. The onboarding cover gets the same treatment: the body it wraps carries a NavigationStack, so it is exposed exactly as the root is, and a fresh install is the first thing to hit it. The upgrade sheet is SwiftUI-only and is left alone. Two comments corrected. The header said "Idle, the layer adds nothing: no effect, no overlay" -- the effect is attached either way, and that sentence is what makes the idle path look innocent. And the extension doc now states the constraint a caller needs: the wrapped content must be SwiftUI only. Verified on an iPhone 15 Pro built with Xcode 27: before, the placeholder at launch; after, the app renders. That the clear overlay cures it is also what distinguishes the two candidate causes -- a shader that failed to resolve would still mark over Color.clear. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AWV4MaF9JBcQppSX9UxATa --- app/network/Main/MainView.swift | 20 +++++++++++++++++-- .../Views/Introduction/IntroductionView.swift | 13 ++++++++++-- .../ProCelebration/ProCelebrationLayer.swift | 14 +++++++++++-- 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/app/network/Main/MainView.swift b/app/network/Main/MainView.swift index aca2cfba..b7fdc6c6 100644 --- a/app/network/Main/MainView.swift +++ b/app/network/Main/MainView.swift @@ -108,8 +108,24 @@ struct MainView: View { } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(themeManager.currentTheme.backgroundColor) - // the Pro celebration over the whole app; sheets and covers host their own layer - .proCelebrationLayer() + // The celebration draws in its own clear overlay rather than wrapping + // this view. The pixelation is a RASTER-LAYER filter: SwiftUI has to + // render the filtered content into an offscreen layer, and content + // backed by UIKit -- which the tab view below is -- cannot go into + // one, so the whole subtree is replaced by the unsupported-view + // placeholder. Wrapping nothing keeps the confetti, which is plain + // SwiftUI, and costs only the mosaic over the root. + // + // allowsHitTesting is OUTSIDE the layer on purpose: Color.clear is + // hit-testable, so without it the app renders perfectly and ignores + // every touch -- a worse fault than the placeholder, and one a + // screenshot cannot show. + .overlay( + Color.clear + .proCelebrationLayer() + .allowsHitTesting(false) + .accessibilityHidden(true) + ) .environmentObject(subscriptionBalanceViewModel) .environmentObject(subscriptionManager) .environmentObject(proCelebration) diff --git a/app/network/Shared/Views/Introduction/IntroductionView.swift b/app/network/Shared/Views/Introduction/IntroductionView.swift index 5a02f836..93aad929 100644 --- a/app/network/Shared/Views/Introduction/IntroductionView.swift +++ b/app/network/Shared/Views/Introduction/IntroductionView.swift @@ -230,8 +230,17 @@ struct IntroductionView: View { } .animation(.easeIn(duration: 0.25), value: subscriptionManager.purchaseSuccess) .animation(.easeIn(duration: 0.25), value: balanceCodeRedeemed) - // the celebration draws over the onboarding cover, which sits above the app root - .proCelebrationLayer() + // Over the onboarding cover, which sits above the app root. Same clear + // overlay as the root: the body below carries a NavigationStack, which + // is UIKit-backed on iOS and so cannot be rendered into the raster + // layer the pixelation needs. A fresh install is the first thing that + // hits this path. + .overlay( + Color.clear + .proCelebrationLayer() + .allowsHitTesting(false) + .accessibilityHidden(true) + ) .onChange(of: deviceManager.isPro) { _ in celebrateIfConfirmed() } diff --git a/app/network/Shared/Views/ProCelebration/ProCelebrationLayer.swift b/app/network/Shared/Views/ProCelebration/ProCelebrationLayer.swift index be31b999..1a824b64 100644 --- a/app/network/Shared/Views/ProCelebration/ProCelebrationLayer.swift +++ b/app/network/Shared/Views/ProCelebration/ProCelebrationLayer.swift @@ -5,8 +5,10 @@ // Hosts the Pro celebration over a view: while a flight is in the air the // view's content is pixelated (a mosaic whose cell grows over the first // 5 s, holds while the confetti flies, and shrinks back over the 5 s after -// it) and the confetti draws above it, sharp. Idle, the layer adds nothing: -// no effect, no overlay. +// it) and the confetti draws above it, sharp. Idle, the layer draws nothing: +// the effect is attached but disabled, and there is no overlay. It is +// attached either way -- see ProPixelation -- which is why the wrapped +// content matters even when nothing is in the air. // // The mosaic cell is an animated value: SwiftUI interpolates it, so the // content under it is not re-rendered every frame (a tab view is UIKit @@ -24,6 +26,14 @@ extension View { /// `ProCelebrationState` launches one. Apply at the app root and inside /// modal presentations (the upgrade sheet, the onboarding cover), which /// draw above the root. + /// + /// The wrapped content must be SwiftUI only. The pixelation is a + /// raster-layer filter and it stays attached while idle, so content backed + /// by UIKit -- a tab view, a NavigationStack, a UIViewRepresentable -- + /// cannot be rendered into the filtered layer and the whole subtree is + /// replaced by the unsupported-view placeholder. Where the content is not + /// SwiftUI only, apply this to a clear overlay above it instead: the + /// confetti still draws, and only the mosaic is lost. func proCelebrationLayer() -> some View { modifier(ProCelebrationLayer()) }