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()) }