Designing resilient dark mode for iOS
Dark mode should feel like the same app under different lighting, not a cosmetic inversion applied at the end of development. On iOS, the system already exposes the user’s preferred appearance, and a well-built app follows it through semantic colours, adaptive assets, and trait-aware components. This approach keeps controls readable, preserves brand identity, and lets users change the setting without restarting the app.
For an app targeting Australia, consistency matters across very different conditions: bright summer daylight in Brisbane, clear skies in Perth, and late train or tram journeys in Sydney and Melbourne. A robust implementation also needs to coexist with Australian English, AU$ amounts, 24-hour time, regional time zones, and daylight-saving changes. Dark appearance should support those local details rather than compete with them.
Start with the system appearance
Begin by leaving overrideUserInterfaceStyle unset so the app inherits the system appearance. The UIUserInterfaceStyle key in Info.plist can force a single style, but it removes user control and is rarely appropriate for a general-purpose app. Reserve an override for a genuinely separate experience, such as a photo-editing workspace where a permanently dark canvas protects image colours.
Most screens should be built with semantic colours such as systemBackground, secondarySystemBackground, label, secondaryLabel, and separator. UIKit resolves these resources against the current trait collection, so they can change when the user switches between light and dark modes. Explicit colours remain appropriate for artwork or brand-specific elements, but ordinary interface layers should rarely need one.
UIKit and SwiftUI apps should both follow the system preference rather than storing a second, conflicting theme setting. In SwiftUI, a Color created from an asset catalog can contain separate light and dark values. In UIKit, named colours and dynamic providers offer equivalent behaviour. This shared model also makes previews, automated tests, and future platform updates easier to support.
Turn brand colours into adaptive design tokens
Create named colours in the asset catalog and give them Any and Dark appearance variants where the brand palette requires a visual change. This keeps raw colour values out of view controllers and SwiftUI views, allowing designers to adjust a token in one place. Each variant still needs a contrast check because a colour that looks balanced on white may become difficult to read against a dark background.
For values calculated at runtime, use a dynamic UIColor provider. The provider receives the trait collection used for drawing and can return an appropriate light or dark colour. Do not check the appearance only once during initialisation and store the result, because that prevents the interface from responding correctly when the system setting changes.
Images, icons, and illustrations need separate consideration. Template images can inherit a semantic tint, while monochrome symbols often work well with dynamic rendering. Photography and brand logos should not be inverted automatically; they need carefully chosen dark variants or brighter artwork when required. If the app also persists preferences, this Core Data guide shows how structured app data can fit into a Swift project without interfering with the visual theme.
Check every visual state
Testing a single home screen is not enough. Navigation bars, tab bars, alerts, action sheets, keyboards, switches, text fields, pickers, charts, empty states, and permission prompts can all reveal contrast or layout problems. An app sold in Australia may display AU$ prices, AEST or AEDT timestamps, and 24-hour time, so these states must remain readable after their backgrounds and labels adapt.
Strong daylight in Brisbane and Perth can expose weak contrast that seems acceptable indoors. Late-night travel on Sydney trains or Melbourne trams presents the opposite concern, where excessive brightness may be uncomfortable. Avoid low-contrast grey text on dark surfaces, but do not assume that pure white everywhere is comfortable either. Check the complete screen rather than judging isolated components.
Use Xcode previews to compare both appearances, then run the app and switch the system setting while it remains active. Accessibility Inspector can help identify contrast and element issues. Increase Contrast, Reduce Transparency, Bold Text, Larger Text, and Differentiate Without Color can change a layout or colour relationship even when the basic light and dark appearances look correct.
UI tests can launch important flows under each appearance, while snapshot tests can catch unintended changes in navigation bars, controls, and custom drawings. These checks are especially valuable for reusable components that appear on many screens. Keep a small set of representative fixtures, such as a long account name, an error state, and a disabled button, instead of trying to duplicate every possible screen.
Compare the main implementation paths
The main implementation paths differ in flexibility, development effort, and long-term maintenance. Semantic system colours cover standard interfaces quickly, named assets support brand consistency, and dynamic providers handle values that depend on runtime conditions.
Forced appearance is the least suitable option for most apps because it overrides an explicit user preference. Choose it only when preserving the appearance of media or a specialised workflow matters more than matching the rest of iOS. The following comparison shows where each approach fits:
| Approach | Best suited to | Main benefit | Main limitation |
|---|---|---|---|
| System semantic colours | Standard UIKit controls and common screens | Automatic adaptation with minimal custom code | Cannot express every brand colour |
| Asset catalog named colours | Brand palettes, badges, charts, and illustrations | One token works across every appearance | Each variant requires contrast testing |
Dynamic UIColor provider |
Custom values calculated at runtime | Flexible without separate asset files | Must resolve against the active trait collection |
| Forced app appearance | A deliberately dark-only experience | Provides a predictable canvas | Conflicts with the user’s system preference |
Keep dark mode maintainable
Centralise colour decisions in named design tokens or a small set of helpers. Scattering appearance checks across view controllers makes every future redesign riskier because one forgotten branch can leave a control with the wrong background. A central token layer also gives testers a clear place to audit contrast and accessibility.
Use this release review before shipping:
- Every screen uses named or semantic colours rather than hard-coded RGB or hex values.
- Dark assets cover icons, illustrations, separators, and selected-state artwork.
- Alerts, action sheets, keyboards, pickers, and empty states work in both appearances.
- UI tests or snapshots include a dark-appearance run and an appearance switch.
Then run a separate accessibility pass:
- Text and essential controls remain legible in normal and increased-contrast modes.
- Layouts accommodate Larger Text, Bold Text, and reduced transparency.
- Status is never communicated by colour alone; text, icons, or shapes provide a backup.
- VoiceOver labels, traits, and focus order remain unchanged in dark mode.
Keep the source of truth simple: use dynamic system resources for platform UI, named tokens for brand decisions, and trait-aware tests to prevent regressions. The standard to remember is to adapt deliberately, preserve contrast, and let the user’s iOS appearance remain authoritative.