Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions app/network/Main/MainView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
13 changes: 11 additions & 2 deletions app/network/Shared/Views/Introduction/IntroductionView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
14 changes: 12 additions & 2 deletions app/network/Shared/Views/ProCelebration/ProCelebrationLayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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())
}
Expand Down
Loading