2

i'm trying to implement ios13 darkmode within multi scene application.

Unfortunately when i dismiss a scene dragging it over the screen edge the method traitCollectionDidChange is called several times with always different values, causing my UI to flicker between dark and light mode.

What's wrong?

Here is my implementation

func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {  
    super.traitCollectionDidChange(previousTraitCollection)  

    print("THEME instance: \(self)")  

    let currentTraitCollection = self.traitCollection  
    var hasUserInterfaceStyleChanged = false  
    hasUserInterfaceStyleChanged = previousTraitCollection.hasDifferentColorAppearanceCompared(to: currentTraitCollection)  

    print("THEME hasUserInterfaceStyleChanged = \(hasUserInterfaceStyleChanged ? "YES" : "NO")")  

    if hasUserInterfaceStyleChanged {  
        let userInterfaceStyle = currentTraitCollection.userInterfaceStyle // Either .unspecified, .light, or .dark  

        switch userInterfaceStyle {  
            case .unspecified:  
                print("THEME UIUserInterfaceStyleUnspecified")  
            case .light:  
                print("THEME UIUserInterfaceStyleLight")  
            case .dark:  
                print("THEME UIUserInterfaceStyleDark")  
            }  
    } else {  
        print("THEME NOT CHANGED")  
    }  

}  

Here is the logged statements in console

When new scene comes in...

THEME instance: <MainControllerViewController: 0x117e55910>  
THEME hasUserInterfaceStyleChanged = YES  
THEME UIUserInterfaceStyleLight  

When added scene goes away...

THEME instance: <MainControllerViewController: 0x117e55910>  
THEME hasUserInterfaceStyleChanged = YES  
THEME UIUserInterfaceStyleDark  
THEME instance: <MainControllerViewController: 0x117e55910>  
THEME hasUserInterfaceStyleChanged = YES  
THEME UIUserInterfaceStyleLight  
THEME instance: <MainControllerViewController: 0x117e55910>  
THEME hasUserInterfaceStyleChanged = NO  
THEME NOT CHANGED  
THEME instance: <MainControllerViewController: 0x117e55910>  
THEME hasUserInterfaceStyleChanged = YES  
THEME UIUserInterfaceStyleDark  
THEME instance: <MainControllerViewController: 0x117e55910>  
THEME hasUserInterfaceStyleChanged = YES  
THEME UIUserInterfaceStyleLight  

in meantime i have no changed to dark mode (always light)...so i expect just THEME NOT CHANGED.

Fabiosoft
  • 1,141
  • 14
  • 32
  • 1
    See https://stackoverflow.com/questions/57993277/evaluating-uitraitcollections-hasdifferentcolorappearancecomparedto-result for possible reasons for so many apparent trait changes. – rmaddy Sep 19 '19 at 07:27
  • 1
    It looks like when a scene is dismissed iPadOS internally wants to capture screens in both userInterfaceStyles thus the multiple calls traitCollectionDidChange(). Typically, this happens rather fast and you're not supposed to see it (much less notice it). Just be careful with what you're actually doing when the userInterfaceStyle changes. Improper tasks can lead to flickering of light and dark mode. I was suffering from that problem because I was using notification pattern to update multiple viewControllers. Doing something more direct corrected the problem. – Phantom59 Nov 24 '19 at 06:18
  • yes, I already solved, but that's the reason! – Fabiosoft Nov 25 '19 at 09:01

1 Answers1

3

I was struggling with the same issue and the solution I worked out is in the SceneDelegate.

A UIScene has multiple states:

.foregroundActive
.foregroundInactive
.background
.unattached

When you are resizing windows in slide over, or in this case, removing one from slide over, traitCollectionDidChange gets called for each of them. This means that you are updating the userInterfaceStyle for scenes in the .background, .foregroundInactive, and .unattached states. This is what's causing the flickering.

The solution is to not use traitCollectionDidChange, but to use a delegate method in SceneDelegate called windowScene(_:didUpdate:interfaceOrientation:traitCollection:).

Per Apple's docs this method:

Notifies you when the size, orientation, or traits of a scene change.

The added benefit of this is that we can check the .activationState of the scene before updating the userInterfaceStyle.

    func windowScene(_ windowScene: UIWindowScene, didUpdate previousCoordinateSpace: UICoordinateSpace, interfaceOrientation previousInterfaceOrientation: UIInterfaceOrientation, traitCollection previousTraitCollection: UITraitCollection) {

          let currentTraitCollection = windowScene.traitCollection

          if windowScene.activationState == .foregroundActive {
             if currentTraitCollection.userInterfaceStyle != previousTraitCollection.userInterfaceStyle {
                 if currentTraitCollection.userInterfaceStyle == .light {
                     //update to light theme
                 } else {
                     //update to dark theme
                 }
             }
         }
     }