A large part of my app consists of web views to provide functionality not yet available through native implementations. The web team has no plans to implement a dark theme for the website. As such, my app will look a bit half/half with Dark Mode support on iOS 13.
Is it possible to opt out of Dark Mode support such that our app always shows light mode to match the website theme?
Yes you can skip by adding the following code in viewDidLoad:
Actually I just wrote some code that will allow you to globally opt out of dark mode in code without having to putz with every single viw controller in your application. This can probably be refined to opt out on a class by class basis by managing a list of classes. For me, what I want is for my users to see if they like the dark mode interface for my app, and if they don't like it, they can turn it off. This will allow them to continue using dark mode for the rest of their applications.
User choice is good (Ahem, looking at you Apple, this is how you should have implemented it).
So how this works is that it's just a category of UIViewController. When it loads it replaces the native viewDidLoad method with one that will check a global flag to see if dark mode is disabled for everything or not.
Because it is triggered on UIViewController loading it should automatically start up and disable dark mode by default. If this is not what you want, then you need to get in there somewhere early and set the flag, or else just set the default flag.
I haven't yet written anything to respond to the user turning the flag on or off. So this is basically example code. If we want the user to interact with this, all the view controllers will need to reload. I don't know how to do that offhand but probably sending some notification is going to do the trick. So right now, this global on/off for dark mode is only going to work at startup or restart of the app.
Now, it's not just enough to try to turn off dark mode in every single MFING viewController in your huge app. If you're using color assets you are completely boned. We for 10+ years have understood immutable objects to be immutable. Colors you get from the color asset catalog say they are UIColor but they are dynamic (mutable) colors and will change underneath you as the system changes from dark to light mode. That is supposed to be a feature. But of course there is no master toggle to ask these things to stop making this change (as far as I know right now, maybe someone can improve this).
So the solution is in two parts:
a public category on UIViewController that gives some utility and convenience methods... for instance I don't think apple has thought about the fact that some of us mix in web code into our apps. As such we have stylesheets that need to be toggled based on dark or light mode. Thus, you either need to build some kind of a dynamic stylesheet object (which would be good) or just ask what the current state is (bad but easy).
this category when it loads will replace the viewDidLoad method of the UIViewController class and intercept calls. I don't know if that breaks app store rules. If it does, there are other ways around that probably but you can consider it a proof of concept. You can for instance make one subclass of all the main view controller types and make all of your own view controllers inherit from those, and then you can use the DarkMode category idea and call into it to force opt out all of your view controllers. It is uglier but it is not going to break any rules. I prefer using the runtime because that's what the runtime was made to do. So in my version you just add the category, you set a global variable on the category for whether or not you want it to block dark mode, and it will do it.
You are not out of the woods yet, as mentioned, the other problem is UIColor basically doing whatever the hell it wants. So even if your view controllers are blocking dark mode UIColor doesn't know where or how you're using it so can't adapt. As a result you can fetch it correctly but then it's going to revert on you at some point in the future. Maybe soon maybe later. So the way around that is by allocating it twice using a CGColor and turning it into a static color. This means if your user goes back and re-enables dark mode on your settings page (the idea here is to make this work so that the user has control over your app over and above the rest of the system), all of those static colors need replacing. So far this is left for someone else to solve. The easy ass way to do it is to make a default that you're opting out of dark mode, divide by zero to crash the app since you can't exit it and tell the user to just restart it. That probably violates app store guidelines as well but it's an idea.
The UIColor category doesn't need to be exposed, it just works calling colorNamed: ... if you didn't tell the DarkMode ViewController class to block dark mode, it will work perfectly nicely as expected. Trying to make something elegant instead of the standard apple sphaghetti code which is going to mean you're going to have to modify most of your app if you want to programatically opt out of dark mode or toggle it. Now I don't know if there is a better way of programatically altering the Info.plist to turn off dark mode as needed. As far as my understanding goes that's a compile time feature and after that you're boned.
So here is the code you need. Should be drop in and just use the one method to set the UI Style or set the default in the code. You are free to use, modify, do whatever you want with this for any purpose and no warranty is given and I don't know if it will pass the app store. Improvements very welcome.
Fair warning I don't use ARC or any other handholding methods.
There is a set of utility functions that this uses for doing method swapping. Separate file. This is standard stuff though and you can find similar code anywhere.
I'm copying and pasting this out of a couple of files since the q-runtime.h is my reusable library and this is just a part of it. If something doesn't compile let me know.
If you will add
UIUserInterfaceStyle
key to the plist file, possibly Apple will reject release build as mentioned here: https://stackoverflow.com/a/56546554/7524146 Anyway it's annoying to explicitly tell each ViewControllerself.overrideUserInterfaceStyle = .light
. But you can use this peace of code once for your rootwindow
object:Just notice you can't do this inside
application(application: didFinishLaunchingWithOptions:)
because for this selector will not respondtrue
at that early stage. But you can do it later on. It's super easy if you are using customAppPresenter
orAppRouter
class in your app instead of starting UI in the AppDelegate automatically.Just add these line in info.plist file:
This will force app to run in light mode only.
First, here is Apple's entry related to opting out of dark mode. The content at this link is written for Xcode 11 & iOS 13:
This section applies to Xcode 11 usage
Approach #1
Use the following key in your info.plist file:
And assign it a value of
Light
.The XML for the
UIUserInterfaceStyle
assignment:Approach #2
You can set
overrideUserInterfaceStyle
against the app'swindow
variable.Depending on how your project was created, this may be in the
AppDelegate
file or theSceneDelegate
.Apple documentation for overrideUserInterfaceStyle
How the above code will look in Xcode 11:
This section applies to Xcode 10.x usage
If you are using Xcode 11 for your submission, you can safely ignore everything below this line.
Since the relevant API does not exist in iOS 12, you will get errors when attempting to use the values provided above:
For setting
overrideUserInterfaceStyle
in yourUIViewController
This can be handled in Xcode 10 by testing the compiler version and the iOS version:
You can modify the above snippet to work against the entire application for Xcode 10, by adding the following code to your
AppDelegate
file.However, the plist setting will fail when using Xcode version 10.x:
Credit to @Aron Nelson, @Raimundas Sakalauskas, @NSLeader and rmaddy for improving this answer with their feedback.