Aswath
Aswath

Reputation: 1490

Is there a way to set a fullScreenCover background opacity?

I need to push a modal view from a button inside a view component, but should only be covering the bottom half of the screen height, the top half a semi transparent background(black with opacity 30%). Setting the opacity for the topmost view inside the fullscreenCover view builder doesnt work. Any help would be appreciated.

struct ContentView: View {
    
    @State var present: Bool = false
    var body: some View {
        
        VStack(spacing: 20) {
            
            Button(action: {
                present = true
            }, label: {
                
                Text("spawn translucent modal")
            })
            .fullScreenCover(isPresented: $present) {
                VStack(spacing: 20) {
                    Spacer()
                        .frame(maxWidth: .infinity, minHeight: 100)
                        .background(Color.black)
                        .opacity(0.3)
                    
                    Text("modal")
                }
                .background(Color.clear)
                
            }
            
            Text("some content")
            Text("some more content")
        }
    }
}

Upvotes: 9

Views: 5598

Answers (5)

Aurevoir
Aurevoir

Reputation: 116

iOS 16.4 provides .presentationBackground to modify your modal backgrounds. Yay to yoinking UIViewRepresentable workarounds :)

Usage

.fullScreenCover(isPresented: $isPresented) {
    YourView()
        .presentationBackground(.black.opacity(0.3))
}

Upvotes: 6

Asperi
Asperi

Reputation: 258385

Here is possible solution

        .fullScreenCover(isPresented: $present) {
            VStack(spacing: 20) {
                Spacer()
                    .frame(maxWidth: .infinity, minHeight: 100)
                    .background(Color.black)
                    .opacity(0.3)
                
                Text("modal")
            }
            .background(BackgroundCleanerView())     // << helper !!
        }

and now helper

struct BackgroundCleanerView: UIViewRepresentable {
    func makeUIView(context: Context) -> UIView {
        let view = UIView()
        DispatchQueue.main.async {
            view.superview?.superview?.backgroundColor = .clear
        }
        return view
    }

    func updateUIView(_ uiView: UIView, context: Context) {}
}

Upvotes: 14

akaffe
akaffe

Reputation: 542

The accepted solution works but isn't ideal, as it requires traversing and guessing the view hierarchy.

A more reliable option would be to use UIViewControllerRepresentable instead of UIViewRepresentable, so that the parent controller can be accessed directly.

        .fullScreenCover(isPresented: $present) {
            VStack {
                Text("modal")
            }
            .background(Background()) // << helper !!
        }
struct Background: UIViewControllerRepresentable {
    
    public func makeUIViewController(context: UIViewControllerRepresentableContext<Background>) -> UIViewController {
        return Controller()
    }
    
    public func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<Background>) {
    }
    
    class Controller: UIViewController {
        override func viewDidLoad() {
            super.viewDidLoad()
            view.backgroundColor = .clear
        }
        
        override func willMove(toParent parent: UIViewController?) {
            super.willMove(toParent: parent)
            parent?.view?.backgroundColor = .clear
            parent?.modalPresentationStyle = .overCurrentContext
        }
    }
}

Upvotes: 17

Egist Li
Egist Li

Reputation: 634

Refer to answer by @jinyu Meng, it works for most cases. However sometimes it fails when the rootVC has been preseting a VC already, so we need to dig a little bit deeper to find the VC which presents nothing to present the modal view.

The working code with VC finding (findDeepestPresentedViewController method) is as following:

import UIKit
import SwiftUI

fileprivate var currentOverCurrentContextUIHost: UIHostingController<AnyView>? = nil

extension View {

public func overCurrentContext(
    isPresented: Binding<Bool>,
    showWithAnimate: Bool = false,
    dismissWithAnimate: Bool = false,
    modalPresentationStyle: UIModalTransitionStyle = .crossDissolve,
    content: () -> AnyView
) -> some View {
    if isPresented.wrappedValue && currentOverCurrentContextUIHost == nil {
        let uiHost = UIHostingController(rootView: content())
        currentOverCurrentContextUIHost = uiHost

        uiHost.modalPresentationStyle = .overCurrentContext
        uiHost.modalTransitionStyle = modalPresentationStyle
        uiHost.view.backgroundColor = UIColor.clear

        if let rootVC = UIApplication.shared.windows.first?.rootViewController {
            findDeepestPresentedViewController(from: rootVC)?.present(uiHost, animated: showWithAnimate, completion: nil)
        }
    } else {
        if let uiHost = currentOverCurrentContextUIHost {
            uiHost.dismiss(animated: dismissWithAnimate, completion: {})
            currentOverCurrentContextUIHost = nil
        }
    }

    return self
}

fileprivate func findDeepestPresentedViewController(from vc: UIViewController) -> UIViewController? {
    if let presentedViewController = vc.presentedViewController {
        return findDeepestPresentedViewController(from: presentedViewController)
    }
    return vc

}
}

Upvotes: 2

Megabits
Megabits

Reputation: 616

I have added an overCurrentContext() function to View which should solve your problem. It can make a modal view transparent, and can also disable the animation:

import UIKit
import SwiftUI

fileprivate var currentOverCurrentContextUIHost: UIHostingController<AnyView>? = nil

extension View {
    
    public func overCurrentContext(
        isPresented: Binding<Bool>,
        showWithAnimate: Bool = false,
        dismissWithAnimate: Bool = false,
        modalPresentationStyle: UIModalTransitionStyle = .crossDissolve,
        content: () -> AnyView
    ) -> some View {
        if isPresented.wrappedValue && currentOverCurrentContextUIHost == nil {
            let uiHost = UIHostingController(rootView: content())
            currentOverCurrentContextUIHost = uiHost
            
            uiHost.modalPresentationStyle = .overCurrentContext
            uiHost.modalTransitionStyle = modalPresentationStyle
            uiHost.view.backgroundColor = UIColor.clear
            
            let rootVC = UIApplication.shared.windows.first?.rootViewController
            rootVC?.present(uiHost, animated: showWithAnimate, completion: nil)
            
        } else {
            if let uiHost = currentOverCurrentContextUIHost {
                uiHost.dismiss(animated: dismissWithAnimate, completion: {})
                currentOverCurrentContextUIHost = nil
            }
        }
        
        return self
    }
}

Example:

struct TestView: View {
    @State var isPresented = false
    
    var body: some View {
        Button(action: {
            isPresented = true
        }, label: {
            Text("Show Modal")
        }).overCurrentContext(isPresented: $isPresented, content: {
            return AnyView ( //Important
                Text("This is the content of the modal view.")
            )
        })
    } 
}

You can also download the file here: gist

Upvotes: 4

Related Questions