Supporting Light Mode, Dark Mode, and System Appearance in iOS
Course: iOS App Development
Topic: Supporting Light Mode, Dark Mode, and System Appearance
Course Link: iOS App Development Course
Demo Link: Register for a Free Course Demo
Website: JustAcademy
1. Introduction
Modern iOS applications should provide a comfortable and consistent experience in different lighting conditions. Apple supports Light Mode and Dark Mode, allowing applications to adapt their colors, backgrounds, text, icons, and controls according to the user's appearance preference.
Light Mode generally uses light backgrounds and dark text, while Dark Mode uses dark backgrounds and lighter foreground elements. iOS can automatically switch between these appearances according to the user's system settings or schedule.
When developing applications with SwiftUI or UIKit, developers should understand how to support both appearances, use semantic system colors, respond to appearance changes, and test the interface for readability and accessibility.
2. Learning Objectives
- Understand Light Mode, Dark Mode, and system appearance.
- Learn how iOS determines the current interface style.
- Use semantic colors and adaptive system backgrounds.
- Support appearance changes in SwiftUI and UIKit.
- Create custom colors that adapt to Light and Dark Mode.
- Manage images, icons, borders, shadows, and text for both modes.
- Test appearance behavior in the iOS Simulator and on real devices.
- Identify common appearance-related problems and their solutions.
3. Understanding Light Mode and Dark Mode
3.1 Light Mode
Light Mode is an interface appearance that typically uses light backgrounds, dark text, and darker icons. It works well in bright environments and is familiar to many users.
Common characteristics:
- White or light-colored backgrounds.
- Dark text for readability.
- Subtle separators and borders.
- Icons and controls with sufficient contrast against light surfaces.
3.2 Dark Mode
Dark Mode uses darker backgrounds and lighter foreground elements. It can be more comfortable in low-light environments and can help maintain a consistent experience with the user's device appearance preference.
Common characteristics:
- Dark or near-black backgrounds.
- Light-colored text and icons.
- Adaptive separators and elevated surfaces.
- Colors adjusted to preserve contrast and visual hierarchy.
3.3 System Appearance
System Appearance allows the app to follow the appearance selected by the user in iOS Settings. When the user changes the device from Light to Dark Mode, an app that supports adaptive appearance can update automatically without requiring a manual refresh.
For most applications, following the system appearance is the recommended default because it respects user preferences and reduces unnecessary settings.
| Feature |
Light Mode |
Dark Mode |
| Background |
Light or white |
Dark or near-black |
| Text |
Usually dark |
Usually light |
| Separators |
Adapted for light surfaces |
Adapted for dark surfaces |
| Images and icons |
Must remain visible on light backgrounds |
Must remain visible on dark backgrounds |
| Configuration |
User-selected or app-specific |
User-selected or app-specific |
4. How iOS Appearance Adaptation Works
iOS provides appearance information through its interface environment. Frameworks such as SwiftUI and UIKit can use this information to choose suitable colors, images, and other visual properties.
There are three common approaches:
- Adaptive system styles: Use built-in colors and controls that automatically adapt to the current appearance.
- Custom adaptive resources: Define separate Light and Dark variants in an asset catalog.
- Explicit appearance configuration: Set a particular interface style for a screen or application when there is a genuine product requirement.
Whenever possible, prefer adaptive resources instead of checking the appearance manually for every visual element.
5. Supporting Appearance in SwiftUI
SwiftUI provides built-in support for adaptive colors and controls. Many standard views automatically respond to the environment's current color scheme.
5.1 Using System Colors
System colors such as .primary and .secondary adjust their appearance according to the environment. System background styles such as .background and .secondary can also help create adaptable interfaces.
import SwiftUI
struct AppearanceExampleView: View {
var body: some View {
VStack(spacing: 16) {
Text("Welcome to My App")
.font(.title)
.foregroundStyle(.primary)
Text("This text adapts to the current appearance.")
.foregroundStyle(.secondary)
Button("Continue") {
print("Continue tapped")
}
.buttonStyle(.borderedProminent)
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(uiColor: .systemBackground))
}
}
Explanation:
Color(uiColor: .systemBackground) uses a UIKit semantic background color that adapts to the current appearance.
.primary provides a prominent foreground style.
.secondary provides a less prominent foreground style.
- The standard button style adapts to the platform's visual conventions.
In a SwiftUI-only interface, you can also use SwiftUI's semantic color styles and background materials. Avoid hardcoding white backgrounds and black text unless the design specifically requires fixed colors.
5.2 Reading the Current Color Scheme
Use the @Environment property wrapper to read the current color scheme when the interface needs to change behavior or display based on the active appearance.
import SwiftUI
struct ColorSchemeExampleView: View {
@Environment(\.colorScheme) private var colorScheme
var body: some View {
VStack(spacing: 12) {
Text("Appearance Demo")
.font(.title)
Text(
colorScheme == .dark
? "Dark Mode is active"
: "Light Mode is active"
)
}
.padding()
}
}
Explanation: The colorScheme environment value is either .light or .dark. SwiftUI updates the view when the environment changes, allowing the displayed message to stay current.
Use this technique when conditional behavior is genuinely necessary. For ordinary text and backgrounds, adaptive system colors are usually simpler and more maintainable.
5.3 Previewing Both Appearances in SwiftUI
Previews help developers check a view in both Light and Dark Mode without repeatedly changing the simulator's system settings.
import SwiftUI
#Preview("Light Mode") {
AppearanceExampleView()
.preferredColorScheme(.light)
}
#Preview("Dark Mode") {
AppearanceExampleView()
.preferredColorScheme(.dark)
}
The .preferredColorScheme(_:) modifier is useful for previews and for cases where a view intentionally needs a particular appearance. Use it carefully: applying it to a larger container can influence the appearance of the presentation or interface hierarchy.
6. Creating Custom Adaptive Colors
Applications often have brand colors, accent colors, status colors, and custom backgrounds. These colors should remain readable and visually appropriate in both appearances.
6.1 Using an Asset Catalog
Xcode asset catalogs allow you to create a color set with separate values for Light and Dark appearances.
- Open the project in Xcode.
- Open
Assets.xcassets in the Project Navigator.
- Click the plus button and create a new Color Set.
- Give the color a meaningful name, such as
AppSurface or BrandAccent.
- Select the color set and locate the appearance configuration in the Attributes Inspector.
- Choose the option that supports Any Appearance and Dark Appearance.
- Set the appropriate color value for each appearance.
- Build and preview the interface in both modes.
To use the named color in SwiftUI, reference the asset by its name.
import SwiftUI
struct CustomColorView: View {
var body: some View {
VStack(spacing: 12) {
Text("Adaptive Brand Interface")
.font(.title)
Text("This surface uses a color asset.")
}
.padding()
.background(Color("AppSurface"))
}
}
Because the asset provides appearance-specific values, the color can update automatically when the active appearance changes.
6.2 Defining Adaptive Colors in Code
When needed, you can create a dynamic color using UIKit's UIColor trait-aware initializer and expose it to SwiftUI.
import SwiftUI
import UIKit
extension Color {
static let adaptiveCardBackground = Color(
UIColor { traits in
traits.userInterfaceStyle == .dark
? UIColor(red: 0.12, green: 0.12, blue: 0.14, alpha: 1)
: UIColor(red: 0.96, green: 0.96, blue: 0.98, alpha: 1)
}
)
}
struct AdaptiveCardView: View {
var body: some View {
Text("Adaptive Card")
.padding()
.background(Color.adaptiveCardBackground)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
This approach can be useful when colors must be defined programmatically. For reusable design systems, named color assets are often easier for designers and developers to maintain together.
7. Supporting Light and Dark Mode in UIKit
UIKit provides dynamic system colors and a trait collection that describes the current environment, including the user interface style.
7.1 Using Dynamic System Colors
import UIKit
class AppearanceViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
let titleLabel = UILabel()
titleLabel.text = "Appearance Support"
titleLabel.textColor = .label
titleLabel.font = .boldSystemFont(ofSize: 24)
titleLabel.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(titleLabel)
NSLayoutConstraint.activate([
titleLabel.centerXAnchor.constraint(
equalTo: view.centerXAnchor
),
titleLabel.centerYAnchor.constraint(
equalTo: view.centerYAnchor
)
])
}
}
Important adaptive UIKit colors include:
| Color |
Purpose |
.systemBackground |
Primary background for a screen or surface. |
.secondarySystemBackground |
Secondary grouped or inset surface. |
.label |
Primary text color. |
.secondaryLabel |
Secondary text color. |
.separator |
Adaptive separator lines. |
.systemBlue |
System blue accent color that adapts as designed by the platform. |
7.2 Detecting Appearance Changes in UIKit
Most UIKit components using dynamic colors update automatically. If custom drawing or other appearance-dependent work must be refreshed, respond to trait changes.
import UIKit
class CustomAppearanceViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
updateCustomAppearance()
}
override func traitCollectionDidChange(
_ previousTraitCollection: UITraitCollection?
) {
super.traitCollectionDidChange(previousTraitCollection)
if traitCollection.hasDifferentColorAppearance(
comparedTo: previousTraitCollection
) {
updateCustomAppearance()
}
}
private func updateCustomAppearance() {
if traitCollection.userInterfaceStyle == .dark {
view.layer.borderColor = UIColor.white.cgColor
} else {
view.layer.borderColor = UIColor.black.cgColor
}
view.layer.borderWidth = 1
}
}
Note: The trait-change APIs have evolved across iOS versions. For newer deployment targets, consult Apple's current UIKit documentation for the recommended trait registration and change-handling API. Avoid depending on a deprecated callback when a newer API is available.
8. Controlling the Interface Style
Most apps should follow the system setting. However, some apps may provide a user-facing appearance preference or require a fixed appearance for a particular experience.
8.1 SwiftUI Appearance Preference
SwiftUI's .preferredColorScheme(_:) modifier can request a specific color scheme for a view hierarchy or presentation.
// Follow the environment by default:
ContentView()
// Request Light Mode for a specific presentation:
ContentView()
.preferredColorScheme(.light)
// Request Dark Mode for a specific presentation:
ContentView()
.preferredColorScheme(.dark)
Do not apply a fixed scheme globally unless the app's requirements justify overriding the user's system preference.
8.2 UIKit Appearance Configuration
UIKit applications can configure the preferred interface style through the view controller or window appearance configuration. The exact setting depends on the intended scope and the project's iOS deployment target.
For an application-wide setting, the UIUserInterfaceStyle key in the app's Info configuration can be used where appropriate. Values commonly include Automatic, Light, and Dark.
Use Automatic when the app should follow the user's device appearance. Before forcing a style, consider whether the app truly needs to ignore the system setting.
9. Supporting Appearance for Images and Icons
Colors are only one part of an adaptive interface. Images, logos, illustrations, and icons must also remain clear in both appearances.
- Use SF Symbols when suitable, because system symbols generally integrate well with native interface styling.
- Use template rendering for monochrome icons when they should inherit a foreground tint.
- Provide separate Light and Dark image variants in asset catalogs when an illustration requires different artwork.
- Avoid transparent logos that disappear against a dark or light background.
- Test image contrast, especially for illustrations with white backgrounds or thin lines.
import SwiftUI
struct IconExampleView: View {
var body: some View {
Label("Settings", systemImage: "gearshape")
.font(.title3)
.foregroundStyle(.primary)
.padding()
}
}
SF Symbols such as gearshape, magnifyingglass, and person.crop.circle can help create familiar native controls. Choose symbols that match the meaning of the action and test their appearance in context.
10. Handling Borders, Shadows, and Cards
Hardcoded borders and shadows may look attractive in Light Mode but become too strong, too subtle, or visually distracting in Dark Mode. Prefer system materials and adaptive colors where possible.
import SwiftUI
struct AdaptiveCard: View {
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Course Details")
.font(.headline)
Text("Learn iOS app development step by step.")
.foregroundStyle(.secondary)
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(.regularMaterial)
.clipShape(RoundedRectangle(cornerRadius: 16))
}
}
Materials such as .regularMaterial can provide a system-adaptive surface. Verify that the result remains readable over the content behind it and that it suits the app's visual design.
11. Accessibility and Readability
Appearance support must do more than change colors. It should preserve readability, meaning, and usability for all users.
- Maintain sufficient contrast between text and its background.
- Do not communicate status using color alone; use text, symbols, or other cues as well.
- Check text at different Dynamic Type sizes.
- Ensure disabled controls remain distinguishable without becoming unreadable.
- Test charts, graphs, error messages, and form validation states in both modes.
- Respect accessibility settings and platform conventions.
A color combination that works in one appearance may fail in the other. Review every important screen rather than assuming that automatic color adaptation guarantees a fully accessible design.
12. Testing Light Mode and Dark Mode in Xcode
- Open the iOS project in Xcode.
- Select an iOS Simulator device.
- Build and run the app.
- Open the simulator's Settings app and locate Display & Brightness.
- Switch between Light and Dark appearances.
- Return to the app and verify that its interface updates correctly.
- Repeat the test for every major screen, navigation flow, sheet, alert, and form.
- Check custom images, charts, buttons, text fields, separators, and disabled controls.
- Test on a real device when possible.
SwiftUI previews can also show the same view in both color schemes, helping developers identify visual problems early in development.
13. Common Problems and Solutions
| Problem |
Likely Cause |
Solution |
| White background remains in Dark Mode |
A fixed white color is used. |
Use an adaptive system background or a color asset with appearance variants. |
| Text becomes invisible |
Foreground and background colors have insufficient contrast. |
Use semantic text colors and verify contrast in both appearances. |
| Logo disappears in Dark Mode |
The image was designed only for a light background. |
Provide an appropriate dark appearance asset or use a suitable template image. |
| Border is too bright or too dark |
A fixed border color is used. |
Use an adaptive color or update the border when the relevant traits change. |
| Some screens adapt but others do not |
Individual screens use hardcoded styles or fixed appearance settings. |
Audit reusable components and remove unnecessary fixed appearance overrides. |
| Appearance does not change as expected |
A color was converted to a non-dynamic value or custom drawing was not refreshed. |
Use dynamic resources and refresh custom-rendered content when traits change. |
14. Best Practices
- Follow the system appearance by default.
- Use semantic colors instead of hardcoded black and white values.
- Create named adaptive colors for brand-specific elements.
- Use reusable components to maintain consistent appearance behavior.
- Keep contrast, hierarchy, and readability consistent in both modes.
- Provide appearance-specific assets when a single image is unsuitable.
- Avoid forcing Light or Dark Mode without a clear product requirement.
- Test alerts, sheets, menus, navigation bars, forms, and empty states.
- Review third-party components because they may not adapt automatically.
- Include Light Mode and Dark Mode checks in the regular quality assurance process.
15. Practical Exercise: Build an Adaptive Profile Screen
Goal: Create a profile screen that automatically adapts to Light Mode and Dark Mode.
Requirements:
- Create a SwiftUI view named
AdaptiveProfileView.
- Add a profile icon, a user's display name, and a short description.
- Use semantic colors for primary and secondary text.
- Use a system or adaptive background for the main screen.
- Add a button with a native button style.
- Preview the screen in both Light and Dark Mode.
- Switch the simulator appearance and verify that all content remains readable.
import SwiftUI
struct AdaptiveProfileView: View {
var body: some View {
VStack(spacing: 16) {
Image(systemName: "person.crop.circle.fill")
.font(.system(size: 64))
.foregroundStyle(.tint)
Text("Alex Morgan")
.font(.title2.bold())
.foregroundStyle(.primary)
Text("iOS App Developer")
.foregroundStyle(.secondary)
Button("Edit Profile") {
print("Edit Profile tapped")
}
.buttonStyle(.borderedProminent)
}
.padding(24)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(uiColor: .systemBackground))
}
}
#Preview("Light") {
AdaptiveProfileView()
.preferredColorScheme(.light)
}
#Preview("Dark") {
AdaptiveProfileView()
.preferredColorScheme(.dark)
}
Expected result: The profile screen should remain readable and visually consistent when the appearance changes, without requiring separate screen implementations.
16. Interview Questions and Answers
Q1. What is Light Mode in iOS?
Light Mode is an interface appearance that generally uses light backgrounds and darker foreground elements.
Q2. What is Dark Mode?
Dark Mode uses darker surfaces and lighter foreground elements to provide an alternative interface appearance.
Q3. What does system appearance mean?
It means the app follows the appearance selected by the user or system, allowing supported interface elements to adapt automatically.
Q4. How do you read the current appearance in SwiftUI?
Use @Environment(\.colorScheme) to read whether the current color scheme is light or dark.
Q5. Why should developers avoid hardcoded white backgrounds?
A hardcoded white background may remain white in Dark Mode, creating poor contrast or an inconsistent interface. Adaptive system colors or color assets are usually better.
Q6. What are semantic colors?
Semantic colors are colors chosen for their interface role, such as primary text, secondary text, or system background, rather than for a fixed RGB value.
Q7. How can you create custom colors for both appearances?
Create a Color Set in the Xcode asset catalog and provide appropriate values for Light and Dark appearances.
Q8. How do you test Dark Mode in Xcode?
Switch the iOS Simulator's appearance in Display & Brightness settings or use SwiftUI previews configured for light and dark color schemes.
Q9. Should every app force Dark Mode?
No. Most apps should respect the user's system preference unless a specific product requirement justifies a fixed appearance.
Q10. What should be checked besides text and backgrounds?
Check icons, images, borders, shadows, charts, navigation elements, controls, alerts, sheets, and accessibility contrast.
17. Quick Revision Checklist
- Light Mode and Dark Mode provide alternative interface appearances.
- System appearance lets an app follow the user's preference.
- SwiftUI supports adaptive appearance through environment values and system styles.
- UIKit provides dynamic system colors and trait information.
- Asset catalogs can store appearance-specific colors and images.
- Semantic colors help preserve contrast and consistency.
- Custom drawing may need to respond to appearance or trait changes.
- Test the entire application in both appearances before release.
18. Conclusion
Supporting Light Mode, Dark Mode, and system appearance is an important part of creating polished iOS applications. By using semantic colors, adaptive asset catalogs, system styles, and appearance-aware components, developers can create interfaces that respect user preferences and remain readable across different environments.
For most applications, the best starting point is to follow the system appearance, avoid unnecessary fixed colors, and test every important screen in both modes. These practices make SwiftUI and UIKit applications easier to maintain, more accessible, and more consistent with native iOS design conventions.
Additional Learning Resources