Reputation: 1490
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
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
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
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
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
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