Mobile Development 11 min read

How JD’s Dark Mode Component Brings Night‑Mode to iOS 9+ Apps

This article explains how JD’s dark‑mode foundation component enables iOS 9+ apps to support system and manual dark mode, detailing its architecture, integration steps, custom updaters, window‑level overrides, snapshot handling, and lessons learned for reliable night‑mode implementation.

JD Retail Technology
JD Retail Technology
JD Retail Technology
How JD’s Dark Mode Component Brings Night‑Mode to iOS 9+ Apps

Background and Goals

iOS 13 introduced a system‑wide dark mode that improves night‑time vision and reduces power consumption, but the built‑in support only works on iOS 13 and later. JD needed a solution that also runs on iOS 9+, offers a manual toggle inside the app, and lets each module adopt the feature through a unified interface.

Component Capabilities

Supports iOS 9 and above while remaining compatible with iOS 13’s native dark mode.

Provides global enable/disable and per‑module downgrade.

Allows following the system theme or using an app‑internal theme.

Includes a built‑in debugging tool for rapid UI verification.

Extensible color‑mode framework.

Component Architecture Diagram
Component Architecture Diagram

Business Integration

Modules integrate the component by calling jdbappearance_bindUpdater with a block that updates UI elements. The component stores the view and block in a hash table; when the view’s window becomes available, it iterates the table and executes the appropriate blocks.

The block runs only if the view’s window property is non‑nil; otherwise the block is marked for later execution and will run when didMoveToWindow is triggered. The block is re‑executed only when the color appearance actually changes.

// Before integration
cell.viewA.backgroundColor = [UIColor redColor];
cell.viewB.image = [UIImage imageNamed:@"xxx"];

// After integration
@weakify(cell)
[cell jdbappearance_bindUpdater:^(JDBAppearance *appearance, UIView *bindView) {
    @strongify(cell)
    cell.viewA.backgroundColor = [UIColor jdbappearance_colorBR];
    cell.viewB.image = [UIImage jdbappearance_imageNamed:@[ @"light_xx", @"dark_xx" ]];
}];

Because the block is executed immediately upon binding, the integration pattern remains identical even in asynchronous scenarios, incurring no extra cost.

Custom Updater API

For convenience, a category on UIView adds jdb_setBackgroundColor:, which internally registers a block with a unique updaterKey so a view can host multiple updaters without retain‑cycle issues.

@implementation UIView (CustomUpdater)

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

@end

Usage example:

[cell jdb_setBackgroundColor:@[@"#FFFFFF", @"#1D1B1B"]];

In‑App Dark‑Mode Switch and Window Override

When the user disables dark mode inside the app while the system remains in dark mode, some system UI components (e.g., UIImagePickerController) retain dark colors, causing visual inconsistency. The solution is to set overrideUserInterfaceStyle on every UIWindow:

If dark mode is enabled, set each window’s overrideUserInterfaceStyle to UIUserInterfaceStyleDark.

If dark mode is disabled, set it to UIUserInterfaceStyleLight.

Because new windows can appear at runtime, the component registers for UIWindowDidBecomeVisibleNotification and applies the appropriate style to the newly visible window.

Listening to System Theme Changes

Simply using traitCollectionDidChange does not fire after manually overriding the window style. To capture system‑wide theme switches, an ObserveWindow subclass overrides traitCollectionDidChange and performs three actions when a color‑appearance change is detected:

Update the app’s internal UI.

Adjust other windows’ overrideUserInterfaceStyle.

Notify business modules to refresh their UI.

@implementation ObserveWindow

- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
    if (@available(iOS 13.0, *)) {
        if ([self.traitCollection hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]) {
            // 1. Update internal UI
            // 2. Update other windows’ overrideUserInterfaceStyle
            // 3. Notify business layers
        }
    }
}

@end

Multi‑Task Snapshot Issue

When the app moves to the background, iOS creates two snapshots—one for dark and one for light—so the multitasking switcher shows the correct appearance. If the app’s internal mode differs from the system mode, the snapshot can be opposite, leading to a mismatched preview.

Investigation showed that traitCollectionDidChange is invoked twice during backgrounding, first with UIUserInterfaceStyleDark then with UIUserInterfaceStyleLight. The snapshots are taken during these calls.

To avoid incorrect snapshots, the component only enables snapshot handling when the “follow system” switch is on. The revised traitCollectionDidChange implementation distinguishes background state and performs UI updates only when appropriate:

-(void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
    if (@available(iOS 13.0, *)) {
        UIApplicationState state = [UIApplication sharedApplication].applicationState;
        if (state == UIApplicationStateBackground) {
            // Background: system will take two snapshots (dark & light)
            JDBAppearanceManager *manager = [JDBAppearanceManager sharedInstance];
            if (manager.followSystemMode) {
                // Update UI; system will snapshot after UI update
            }
        } else {
            // Foreground changes: system UI toggle, debug menu, etc.
            if ([self.traitCollection hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]) {
                // 1. Update internal style
                // 2. Update other windows’ overrideUserInterfaceStyle
                // 3. Notify business layers
            }
        }
    }
}

Customization Options

The component is designed for reuse beyond JD’s own app. It supports:

In‑app toggle switch.

Multi‑task snapshot control.

Custom updater extensions.

Custom color‑mode definitions.

Conclusion

The article shares the pitfalls encountered while adapting JD’s iOS app to dark mode and presents a complete, extensible solution that other developers can adopt to avoid similar issues.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Mobile DevelopmentiOSObjective‑CUIKitDark ModeTheme SwitchingUI appearance
JD Retail Technology
Written by

JD Retail Technology

Official platform of JD Retail Technology, delivering insightful R&D news and a deep look into the lives and work of technologists.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.