I've made a ViewModifier
for my app so that when it goes into .background
phase it shows a splash of an image. However, when a .sheet
or a .fullScreenCover
is presented it doesn't seem to be triggering the modifier or detecting scenePhase changes.
I've seen that you have to pass the environment from presentation to presentation which works for a 1-2 views, but if I want it to operate on the ViewModifier is it possible? Or is this a bug?
I feel like having to pass either .maskView()
to every screen or passing the @Environment()
or .environment()
to every view too seems wrong for Swift.
struct TestOverlay: View {
@State private var showSheet: Bool = false
var body: some View {
Button("Show sheet") {
showSheet = true
}
.sheet(isPresented: $showSheet) {
Button("Hide sheet") {
showSheet = false
}
}
.maskView()
}
}
struct MaskingViewModifier: ViewModifier {
@Environment(\.scenePhase) private var scenePhase
@State private var currentPhase: ScenePhase = .active
func body(content: Content) -> some View {
ZStack {
content
.blur(radius: currentPhase == .active ? 0 : 5)
Color(.systemBackground)
.ignoresSafeArea()
.opacity(currentPhase == .active ? 0 : 0.4)
Image(systemName: "xmark")
.resizable()
.shadow(radius: 5)
.frame(width: 180, height: 180)
.opacity(currentPhase == .active ? 0 : 1)
}
.ignoresSafeArea()
.animation(.default, value: currentPhase)
.onChange(of: scenePhase) { phase in
switch phase {
case .active:
currentPhase = .active
case .inactive:
currentPhase = .inactive
case .background:
currentPhase = .background
@unknown default:
currentPhase = .active
}
}
}
}
extension View {
func maskView() -> some View {
return modifier(MaskingViewModifier())
}
}
If onChange(of: scenePhase)
does not get triggered this is indeed a bug. (Edit: I think it only works on the App
struct level.)
Note that you do not have to pass environments for every child view, but only when presented as sheets
.
The masking of the view will however never hide the sheet as the sheet is presented above the view modifier. The order of applying .sheet
does not matter unfortunately.
One solution would be to fall back to UIKit
, and present the blur on an overlaid UIWindow
.
You could use something like this:
... // some View
var window: UIWindow?
var body: some View {
someView
.onChange(of: scenePhase) { phase in
// some logic if blur should be showed:
UIApplication.shared.presentOnOverlayWindow(SomeBlurView())
}
}
...
extension UIApplication {
/// Returns a window to wich a strong reference needs to be kept for the duration of the presentation of the view controller.
/// Otherwise the window is deallocated and removed from the view hierarchy.
static func presentOnOverlayWindow<V: View>(_ view: V) -> UIWindow? {
let scene = UIApplication.shared.connectedScenes.first { $0.activationState == .foregroundActive }
?? UIApplication.shared.connectedScenes.first { $0.activationState == .foregroundInactive }
?? UIApplication.shared.connectedScenes.first
guard let windowScene = scene as? UIWindowScene else {
return nil
}
let window = UIWindow(windowScene: windowScene)
let rootViewController = UIViewController()
window.rootViewController = rootViewController
window.makeKeyAndVisible()
rootViewController.present(UIHostingController(rootView: view), animated: true)
return window
}
}
This is only presenting the blur, but you will need to also remove it through the stored UIWindow
ref.