What is Dark Mode?

In iOS 13, Apple introduced dark mode, which can better protect eyesight at night and save App battery consumption. However, The dark mode provided by Apple only supports iOS 13. To make the experience better for users, we hope that iOS 13 and below can also support Dark mode. In addition, we also provide users with the right to choose, you can manually turn off the dark mode in the App, do not follow the system theme change.

Jingdong App involves a large number of business modules and the whole adaptation requires a huge amount of work. In order to solve the above problems and enable all modules to access quickly through unified interfaces, we have developed the Diablo basic component, which provides the following capabilities:

  • Support iOS 9 or later, compatible with iOS 13 diablo mode
  • Support overall cutting and downgrade
  • Support to follow the system mode, you can also choose not to follow, use the mode inside the App
  • Built-in debugging tools to help developers debug quickly and improve efficiency
  • Support color mode extension

The basic component design scheme is as follows:

Business access

During business access, jdbappearance_bindUpdater method provided by the base component is required to be called, and a Block is introduced and UI update logic is processed therein. The base component will bind Block and UIView, and then UIView is stored in HashTable. Update the UI when appropriate by iterating over the HashTable and performing bound blocks. The solution for connecting service components is as follows:

If the window has a value, it executes the UIView bound Block. If the window has a value, it executes the UIView bound Block. If the window has a value, it executes the UIView bound Block. This Block is executed the next time UIView appears in a window (when didMoveToWindow is called).

Also, don’t worry about the Block being called every time it didMoveToWindow, because the Block is marked for later execution only when the color mode changes.

If asynchronous scenarios such as interface calls are involved, will the cost of access increase? Let’s take a look at how the business fits in with the following code example:

/ / access before cell. ViewA. BackgroundColor = [UIColor redColor]; cell.viewB.image = [UIImage imageNamed:@"xxx"]; @Weakify (cell) [Cell jdbappearance_bindUpdater:^(JDBAppearance *apperance, UIView *bindView) {
     @strongify(cell)
    cell.viewA.backgroundColor = [UIColor jdbappearance_colorBR];
    cell.viewB.image = [UIImage jdbappearance_imageNamed:@[@"light_xx"The @"dark_xx"]];
}];
Copy the code

Since jdbappearance_bindUpdater will execute a Block immediately every time it is called, the access method is uniform regardless of whether asynchronous scenarios are involved and there is no additional access cost.

Custom Updater:

The Block mechanism can basically satisfy all adaptation scenarios, but in practical development, we may want to have some convenient methods, such as calling a method jd_setBackgroundColor to set the background color of UIView.

This requirement can also be satisfied, so let’s look at how to encapsulate such an API:

 @implementation UIView (CustomUpdater)
 
 
 - (void)jdb_setBackgroundColor:(NSArray *)colors
 {
     [self jdbappearance_bindUpdater:^(JDBAppearance * _Nonnull appearance, UIView * _Nonnull bindView) {
         bindView.backgroundColor = [UIColor jdbappearance_colorWithHex:colors];
     } updaterKey:@"jdb_setBackgroundColor"];
 }


@end
Copy the code

Note that you need to specify an updaterKey when binding blocks. UpdaterKey allows a UIView to bind multiple blocks. It’s easy to use and doesn’t need to worry about circular references:

[cell jdb_setBackgroundColor:@[@"#FFFFFF"The @"#1D1B1B"]].Copy the code

Switch to dark mode in App

This feature allows users to manually turn dark mode on or off within the App, but there is a problem:

If diablo is turned on in the system but is turned off in the App, some system controls will still be dark in color (for example, the system photo album is called up by UIImagePickerController), causing the color of the system controls to be inconsistent with the color of the App.

Before explaining the solution, let’s introduce UITraitCollection:

UITraitCollection is a new class in iOS 8 that manages the system characteristics of the user interface in an App. Each view has its own UITraitCollection.

Information about iOS 13 color modes is stored in the userInterfaceStyle property. If we want to give the view specified userInterfaceStyle alone, you need to use the iOS 13 new API overrideUserInterfaceStyle, additional Settings take effect overrideUserInterfaceStyle is on view.

But with so many views, whose properties should we modify? The following diagram illustrates the hierarchy of views and the route of UITraitCollection:

  • If open the diablo, set all window overrideUserInterfaceStyle to UIUserInterfaceStyleDark.
  • If diablo, off the set of all Windows overrideUserInterfaceStyle to UIUserInterfaceStyleLight.

If after overrideUserInterfaceStyle modification, a new window appears, how to deal with this situation? Registered the UIWindowDidBecomeVisibleNotification advised us that this notification will be sent when a UIWindow object visible, after receiving the notice, Set the window overrideUserInterfaceStyle properties.

Conclusion: by modifying the overrideUserInterfaceStyle properties of Windows, most of the color of the control system can be consistent with the color of the App.

Listen for system mode switching

Why bring that up? TraitCollectionDidChange?

Because we found that, after modification overrideUserInterfaceStyle when switching system color mode, the window and its child views traitCollectionDidChange have not been called.

Although the official document and found no clear instructions, but after verification, as long as we set the window overrideUserInterfaceStyle to UIUserInterfaceStyleDark or UIUserInterfaceStyleLight, We can’t listen to the window or its subviews. Only the default UIUserInterfaceStyleUnspecified will take effect.

So what to do? We have just changed all window overrideUserInterfaceStyle 😂 😂 😂

The way is always more than difficult! To analyze carefully, we modify the window overrideUserInterfaceStyle is to modify the color of the control system simultaneously. That we can create a separate ObserveWindow, at the time of switching mode, if it is ObserveWindow skip, only change the other overrideUserInterfaceStyle window. This implements the traitCollectionDidChange method in ObserveWindow, which handles the logic for listening for system mode switches and updating the App UI:

 @implementatiton ObserveWindow
 
 
 - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
 {
     if(@ the available (iOS 13.0, *)) {if([self.traitCollection hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]) { // 1. Modify the internal style/App / 2. Modify the other window overrideUserInterfaceStyle / / 3. Notifies the business to update UI}}} @endCopy the code

Multi-task interface snapshot

In the process of adaptation, we found a problem: in the multi-task interface, the color displayed by the App was opposite to the color mode of the system.

After further analysis, we found that traitCollectionDidChange was executed twice when the App entered the background, The two system in the process of execution userInterfaceStyle UIUserInterfaceStyleDark and UIUserInterfaceStyleLight, respectively.

Why is that? Let’s look at the stack when traitCollectionDidChange is called:

There is a switch in the App to control whether to follow the system color mode. When the user chooses to switch to dark mode for the first time, the following system will be enabled by default, and the App mode will be consistent with the system mode. If the switch of “Follow the system” is turned off, the system mode will not be monitored. The mode selected by the user in the App will prevail.

When the switch of “follow the system” is turned off, the color mode in the App may be inconsistent with that in the system. When there is inconsistency, the snapshot will be wrong, for example, Dark mode intercepts the picture in Light mode. To avoid this error, we added a judgment condition that the snapshot function will only be enabled if “Follow the system” is enabled.

The traitCollectionDidChange implementation is as follows:

 -(void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
 {
     if (@available(iOS 13.0, *)) {
   UIApplicationState state = [UIApplication sharedApplication].applicationState;
         if(state = = UIApplicationStateBackground) {/ / system to switch to the background, JDBAppearanceManager * Manager = [JDBAppearanceManager sharedInstance];if(manager.followSystemMode) {// If you follow the system, update the UI, the system will take a snapshot after the UI update is complete}}else{// Trigger scenario: System control center switch mode, background enter foreground, Xcode debug menu switch modeif([self.traitCollection hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]) { // 1. Modify the internal style/App / 2. Modify the other window overrideUserInterfaceStyle / / 3. Notify the service to update the UI}}}}Copy the code

Personalized customization

The positioning of the basic components, in addition to providing support for the adaptation of the dark mode of JINGdong App, we also hope that it can be used by more apps. In addition to supporting existing functions, dark base components also support personalized customization functions or apis. Access parties can flexibly choose according to their own needs:

  • App internal switch
  • Multi-task snapshot
  • Custom Updater
  • Custom color modes

I hope you will not repeat the pit mining

This paper introduces in detail the pits in the adaptation process of jingdong App iOS dark mode, as well as the implementation principle of the whole scheme, hoping to be helpful to everyone.

Welcome to”Jd Zhilian cloud”Learn more!