In one of our SwiftUI projects, we intensively use UIViewController to present a SwiftUI View modally, and it works perfectly under normal circumstances.
However, we have observed that when screen mirroring is enabled on the iPhone, the @Environment viewControllerHolder
becomes nil, preventing the proper presentation of another view. Xcode (14.5) does not flag any issues with the code, and our project is set to build for iOS 17.5.
Without changing too many codebase, is there a way to fix this unexpected issue?
import SwiftUI
import UIKit
struct ContentView: View {
@Environment(\.viewController) private var viewControllerHolder: UIViewController?
@State var presentSecondPage = false
var body: some View {
VStack(spacing: 40) {
Text("This is First Page")
Button("Present Second Page") {
presentSecondPage = true
}
}
.onChange(of: presentSecondPage) {
if presentSecondPage {
viewControllerHolder?.present(style: .fullScreen) {
SecondPage(presentSecondPage: $presentSecondPage)
}
}
}
}
}
struct SecondPage: View {
@Environment(\.viewController) private var viewControllerHolder: UIViewController?
@Binding var presentSecondPage: Bool
var body: some View {
VStack(spacing: 40) {
Text("This is Second Page")
Button("Back to First Page") {
presentSecondPage = false
viewControllerHolder?.dismiss(animated: true)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.gray)
}
}
struct ViewControllerHolder {
weak var value: UIViewController?
}
struct ViewControllerKey: EnvironmentKey {
static var defaultValue: ViewControllerHolder {
let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
let rootVC = windowScene?.windows.first(where: { $0.isKeyWindow })?.rootViewController
return ViewControllerHolder(value: rootVC)
}
}
extension EnvironmentValues {
var viewController: UIViewController? {
get { return self[ViewControllerKey.self].value }
set { self[ViewControllerKey.self].value = newValue }
}
}
extension UIViewController {
func present<Content: View>(style: UIModalPresentationStyle = .automatic, @ViewBuilder builder: () -> Content) {
let toPresent = UIHostingController(rootView: AnyView(EmptyView()))
toPresent.modalPresentationStyle = style
toPresent.rootView = AnyView(
builder()
.environment(\.viewController, toPresent)
)
NotificationCenter.default.addObserver(forName: Notification.Name(rawValue: "dismissModal"), object: nil, queue: nil) { [weak toPresent] _ in
toPresent?.dismiss(animated: true, completion: nil)
}
self.present(toPresent, animated: true, completion: nil)
}
}
Normal Circumstances
Screen Mirroring
The concept of an application wide key window is flawed in the presence of scenes because you can have multiple key window (and the “application” level one may not be the one you are currently looking at).
let rootVC = windowScene?.windows.first(where: { $0.isKeyWindow })?.rootViewController
Consider that there may be 2 scenes from an app on screen at the same time. In this case, it would be extremely arbitrary. It basically means you want to get the rootViewController of some random connected scene.
The only way to do this is to keep reference of the rootViewController defined when you set the window of your Scene
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = /* Some View controller */
self.window = window
window.makeKeyAndVisible()
}
}
}