Swift Automatically Switch App’s Theme At Sunset - ios

There are several apps on the App Store that have the option in the app to automatically switch to dark mode. I’m assuming there is an easy way to implement this, but I can’t seem to find anything about it when looking online. I just want to be able to have the user select an option that will automatically switch to a dark theme if it is after sunset but have the light theme during the day. Anyone know how to do this in swift?

The most reliable – and by far the easiest – way to implement this is to plug into the system's dark mode setting, rather than rolling your own time-based system. This will also allow your users to make their own decision – if they want light mode all the time, or want to temporarily switch to dark mode in the middle of the day, your app will automatically work for them. And they'll thank you for it.
There are various ways you can manage this:
Use Color's built-in definitions, such as .primary and .secondary, and when they don't satisfy, consider using init(UIColor) to convert some of UIKit's larger list of UI Element colors to work with SwiftUI.
If you define your own colors in your Asset catalog and use them via the Color("My Color Name") syntax, you can elect to set a dark mode variant to use (by changing the Appearance setting from None to Any, Dark).
When the iOS system changes from light to dark and back, your color variants will get selected automatically. These might be a slight variation – e.g., a different shade of purple that looks better when working with dark colors – or it could be something that inverts completely (light gray to dark gray, for example).
If you need to make any more drastic changes – like changing line weights in dark mode – your views can find out whether dark mode is enabled by using the colorScheme environment variable:
struct MyView: View {
#Environment(\.colorScheme) private var colorScheme
// ...
}
colorScheme will be either .dark or .light depending on the phone settings.
However, if you are insistent that you want to have control over light and dark mode instead of letting the system take on that responsibility, you can use the same environment value from step 3 and override it. For example:
struct ContentView {
var body: some View {
MyView()
.environment(\.colorScheme, .dark)
}
}
In that example MyView, and its children, will then behave as if dark mode is always on and everything else – auto-selection of colors from the asset catalog, etc. – will work.
So if you wanted to, you could come up with your own time-based approach to determine whether to set the colorScheme environment to .light or .dark.
However, I'd recommend again to let the system handle everything unless you have a very good reason not to.

If you want to make your own, custom dark mode you can use traitCollectionDidChange to detect when iOS goes in and out of dark mode and then perform your customisation. This is only for iOS 13 and above.
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection) //always call this first
}

Related

Swift: If traitCollection.userInterfaceStyle == .unspecified, how do I determine if it's in light or dark mode?

In the settings of my app, the user can choose between 3 system themes: light, dark, or default (which matches the phone's theme).
I do this by finding the keyWindow and setting overrideUserInterfaceStyle = .dark for dark mode, overrideUserInterfaceStyle = .light for light mode, and overrideUserInterfaceStyle = .unspecified for default.
The problem I'm having is I'm using MapBox within my app and I have both a darkStyleURL and a lightStyleURL. When the user chooses a theme I update the style url as follows:
self.styleURL = self.traitCollection.userInterfaceStyle == .dark ? URL(string: darkStyleURL) : URL(string: lightStyleURL)
But the problem with doing it this way is if the user chooses default as the theme, the traitCollection.userInterfaceStyle is going to be equal to .unspecified. So when the code above triggers, it'll use the lightStyleURL EVEN IF the user's device is in dark mode.
So my question is, after setting overrideUserInterfaceStyle = .unspecified is there another way to determine the user's device theme?
Or is there a better way of handling the use case of matching the device theme or switching between all three options? Any help will be greatly appreciated. Thanks!
You can use UIScreen.main.traitCollection.userInterfaceStyle to get the current device theme independent of the overridden appearance on the window.

Custom Dark Mode iOS Issue

Three enums are set to achieve:
UIUserInterfaceStyleUnspecified -> To listen to iOS setting mode change
UIUserInterfaceStyleLight -> To override LIGHT mode irrespective of iOS setting mode
UIUserInterfaceStyleDark -> To override DARK mode irrespective of iOS setting mode
The problem arises when I set
(Parent_ViewController -> UIUserInterfaceStyleDark) and
(Child_ViewController -> UIUserInterfaceStyleUnspecified)
And then when we switch the settings iOS mode from light to dark or vice versa, there's no change in UI of Child_ViewController and being UIUserInterfaceStyleUnspecified it should've changed but it always has dark theme colors.
Any workarounds or solution is there to address this problem?
Welcome!
ViewControllers will stop propagating the system changes to child controllers when they override the interface style. This also means that a child VC will inherit the interface style of its parent when overrideUserInterfaceStyle is set to .unspecified.
I'm afraid you need some custom implementation if you want to achieve that specific behavior. For instance, by setting the parts you always want to be dark to concrete (undynamic) colors instead of using dynamic system colors.

How can I access UIImage dark appearance pre iOS13

I've just added dark mode to my app.
Asset catalogs support multiple appearances for each asset, on iOS 13 this means the correct asset is used when the system is running in dark or light mode.
These are not my real assets
I'm trying to support dark mode on older iOS versions.
In my attempts to do so, I have added an override to force dark mode which works for my custom colours and theming but not for the images.
Is it possible to access the dark appearance of an image programatically before iOS13?
For iOS12 I have tried using the following:
if #available(iOS 12.0, *) {
let traits = UITraitCollection(userInterfaceStyle: .dark)
let image = UIImage(
named: "Image",
in: bundle,
compatibleWith: traits
)
}
This only returns the normal appearance, and the method naming seems to suggest this only checks that the trait collection I've passed is compatible with the image.
As far as I know, there's no way to do that with just one asset. Below iOS 13 the system will always take the Any appearance. You would need to create two different image sets with different names and choose either one of them.
It is a bit confusing since UIUserInterfaceStyle is available in iOS 12+, but this is likely because macOS got Dark Mode that year.

List with system background color gets messed up in dark mode

I am trying to set a background color for a List that will adapt to the iOS mode (light/dark).
I use .systemGray5 without problems in VStacks but when using it in Lists and I change to dark mode I get a very dark, almost black color which makes everything unintelligible.
This happens regardless if the list is dynamic or static. Is this a bug? Or is there an alternative way to do it?
List {
Text("Privacy").foregroundColor(Color(.systemRed))
}.colorMultiply(Color(.systemGray5))
The problem is that colorMultiply will multiply all colors in the list (text, background, separator) with the given color (see multiply color blending). This will darken the whole view, which probably looks okayish in light mode, but is the opposite of what you want to do in dark mode.
There are two ways to change the background colors in a List:
List {
Text("Privacy").foregroundColor(Color(.systemRed))
.listRowBackground(Color(.systemGray5))
}
.background(Color(.systemGray5))
background will change the background of the whole List, but I guess this is only really visible in grouped lists, since the cells usually don't have any space between them.
listRowBackground changes the background color of a view when it's used in a List environment. That's probably what you want to use here.
Frank Schlegel provided an answer which works only for static lists. Until Apple fixes this for dynamic lists I solved my problem by using this code
List {
ForEach(pumpData) { pump in
Text("pump").listRowBackground(Color(.systemGray5))
}
}.background(Color(.systemGray5))
.secondarySystemGroupedBackground worked for me.

In iOS 13 - How can we implement more than two color modes / themes?

Looking at the brand new WWDC video Implementing Dark Mode on iOS we see that it can be handled quite easily. We can use the new dynamic system colors and they will have specific values depending on Light or Dark Mode.
Now, how can this new concept be combined with the need to implement more than two themes – in a clean and future-proof way?
I have have researched and applied different theming approaches in the past like the UIAppearance approach (e.g. Ray Wenderlich´s UIAppearance Tutorial) or a protocol-based custom dynamic colors approach.
Is the following code an appropriate position to start by finding out when the system requires a switch from light to dark mode and vice versa? And then by changing app wide colors in any of the possible "old" ways?
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) {
/// Really have to go from here?
}
}

Resources