How to animate dark mode change in iOS?

Viewed 1540

Users can switch theme in one section of app settings as showed below.

This is done by changing window.overrideUserInterfaceStyle.

While it works as expected, the change has no animation at all. The whole app goes to the selected style immediately as opposed to the smooth changes in iOS Settings app.

Simply place the assignment in the completion of UIView.animate(withDuration duration: TimeInterval, animations: @escaping () -> Void, completion: ((Bool) -> Void)? = nil) won't work.

app setting

5 Answers

There is an easy way to change the app dark mode theme with animation

if let window = UIApplication.shared.keyWindow {
    UIView.transition (with: window, duration: 0.3, options: .transitionCrossDissolve, animations: {
        window.overrideUserInterfaceStyle = .dark //.light or .unspecified
    }, completion: nil)
}

You could do something like the following:

var toggleVar = true

@IBAction func buttonDidTap(_ sender: UIButton) {
    
    UIView.animate(withDuration: 0.5, animations: {
        self.overrideUserInterfaceStyle = self.toggleVar ? .dark : .light
        self.view.backgroundColor = .systemBackground
    }) { (_) in
        //set overrideUserInterfaceStyle app wide
        UIApplication.shared.windows.first?.overrideUserInterfaceStyle = self.overrideUserInterfaceStyle
        self.toggleVar.toggle()
    }
}

So in order to animate the background change, you need to animate self.view.backgroundColor = .systemBackground. In the completion handler you can set the new overrideUserInterfaceStyle then for the entire app...

I had the same problem. After fruitless attempts with UIView/UIWindow.animate/transition and animation options, I worked around the issue as follows:

  • Capture the whole screen
  • Add a fullscreen UIImageView with the captured image
  • Switch the user interface style in the background (which doesn't animate)
  • Fade out the UIImageView

Assuming you're also working with a UITableViewController within a UINavigationController, an example:

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    // Disable all checkmarks
    tableView.visibleCells.forEach { (cell) in
        cell.accessoryType = .none
    }
    
    // Set checkmark
    let cell = tableView.cellForRow(at: indexPath)
    cell?.accessoryType = .checkmark
    
    // Capture and add screenshot of current screen
    let imageView = UIImageView()
    imageView.image = UIApplication.shared.makeSnapshot()
    imageView.addTo(self.navigationController!.view).snp.makeConstraints { (maker) in
        maker.edges.equalToSuperview()
    }
    
    // Set new dark mode setting and fade out screenshot
    UIView.animate(withDuration: 0.5, animations: {
        // Important to set it within the animations closure, so that the status bar will animate
        UIApplication.shared.windows.first?.overrideUserInterfaceStyle = .dark
        imageView.alpha = 0
    }) { (_) in
        imageView.removeFromSuperview()
    }
    
    // Deselect row
    tableView.deselectRow(at: indexPath, animated: true)
}

Notes:

  • The makeSnapshot() method is from this post: https://stackoverflow.com/a/40953026/4489859
  • I use SnapKit to add/lay out views, so you probably need to change that line accordingly for your project
  • Change .dark to the correct dynamic value for your project

Result:

On the device itself, it's even smoother than the system's change animation for me!

I use Xamarin and for me the solution is:

UIView TempView = Window.SnapshotView(true);    

Window.Add(TempView);

Window.OverrideUserInterfaceStyle = UIUserInterfaceStyle.Light;

UIView.Animate(0.5,
() => { 
    TempView.Alpha = 0;
},
() => {
    TempView.RemoveFromSuperview();
    TempView.Dispose();
});

Swift 5

@objc func changeStyleHandler() {
    
    guard let window = UIApplication.shared.windows.first else { return }
    
    let newInterfaceStyle: UIUserInterfaceStyle = (traitCollection.userInterfaceStyle == .dark) ? .light : .dark
    
    UIView.transition (with: window, duration: 0.3, options: .transitionCrossDissolve, animations: {
        window.overrideUserInterfaceStyle = newInterfaceStyle
    }, completion: nil)
    
}
Related