My iOS and Mac Catalyst app uses a dark color theme by default, regardless of the system color theme. When the app presents a modal view controller (a popup or a popover) on iOS, the background of that view is dark for its entire visible lifespan. But on macOS, if the system color theme is light, the background of the popup is initially white until the view loads, and the background of a popover changes to white as it dismisses:
In my modal view controllers, I'm overriding the color theme, as well as manually setting the background color. This does the job on iOS but has no effect on macOS:
override func viewDidLoad() {
super.viewDidLoad()
self.overrideUserInterfaceStyle = .dark
self.view.backgroundColor = myColor
}
I'm setting overrideUserInterfaceStyle
that way on the main view controller; that handles the main view but doesn't affect popups and popovers.
I tried setting overrideUserInterfaceStyle
on the app's window object, but that had no effect. I tried setting it in application:didFinishLaunchingWithOptions:
for the AppDelegate, in scene:willConnectTo:options:
for the SceneDelegate and even in viewDidLoad
for the modal view controller.
In the xib files that I use for popups and popovers, I tried setting the background of the top-level view to my desired background color, but that had no effect. (Normally I set them to transparent, which is fine in iOS.)
Apparently macOS displays an empty view before loading the xib and before running viewDidLoad. Is there a "deeper" way I can set the color to avoid this ugly visual effect?
UPDATE
Here's the minimum code that demonstrates the problem:
let test = UIViewController()
App.mainViewController.present(test, animated: true)
This results in a white popup window if the Mac is using the light color theme, even though the main view controller's overrideUserInterfaceStyle
is set to dark. I tried setting overrideUserInterfaceStyle
at various levels, but with no change:
App.window?.overrideUserInterfaceStyle = .dark
App.mainViewController.overrideUserInterfaceStyle = .dark
let test = UIViewController()
test.overrideUserInterfaceStyle = .dark
test.view.rootView.overrideUserInterfaceStyle = .dark
test.view.window?.overrideUserInterfaceStyle = .dark
App.mainViewController.present(test, animated: true)
If I set the background color of the popup's view, that works, but it initially appears white and then the background color takes effect, as in the animation above.
let test = UIViewController()
test.view.backgroundColor = App.bgColor
App.mainViewController.present(test, animated: true)
On iOS, all I needed was to set overrideUserInterfaceStyle
on the main view controller.
I consider this a Mac Catalyst bug. I strongly urge you to file a bug report with Apple. Include a simple sample app that demonstrates the problem. All you need is a bare-bones iOS app that adds a button that presents a second view controller. Set the main scene's window's overrideUserInterfaceStyle
property to dark
. Setup the project with a Mac Catalyst destination. Include instructions to run the app while the device is in light-mode.
I did a lot of digging into this issue. The problem stems from the fact that under Mac Catalyst, when presenting a modal view controller with either .formSheet
or .pageSheet
, the app actually creates a new, private scene with its own window that displays the view controller. This new scene is what is displaying that white pane briefly before the actual view controller is displayed.
I attempted to work around the issue with a line such as:
vc.presentationController!.traitOverrides.userInterfaceStyle = .dark
where vc
is the view controller being presented. Despite setting this, the white pane still appears briefly.
I did find one hacky workaround. The solution is to display the view controller as a popover instead of the default .formSheet
. The following code gives reasonable results:
let vc = SomeViewController()
#if targetEnvironment(macCatalyst)
vc.modalPresentationStyle = .popover
vc.isModalInPresentation = true // optional
vc.preferredContentSize = .init(width: 600, height: 600) // matches normal formSheet
vc.popoverPresentationController?.sourceView = self.view
// Calculate where the top-center of the popover should be to get the popover centered in place
let origin = CGPoint(x: self.view.bounds.midX, y: self.view.bounds.midY - vc.preferredContentSize.height / 2)
vc.popoverPresentationController?.sourceRect = .init(origin: origin, size: .zero)
#endif
self.present(vc, animated: true)
When run on a Mac, the view controller is shown as a properly sized and positioned popover. There is no more flash of white.
When run on an iPhone/iPad, the normal presentation is made.
If you have a lot of places in your code where you present a form sheet, you may wish to create some sort of UIViewController
extension method that encapsulates this workaround so you don't need to replicate the code in more than one place.