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.
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.
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"];
}
@endUsage 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
}
}
}
@endMulti‑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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
