Implementing Dark Mode Support in SwiftUI

Dark Mode has become a popular feature in modern applications, offering users a visually comfortable experience, especially in low-light environments. Implementing Dark Mode support in your SwiftUI app enhances accessibility, reduces eye strain, and can even save battery life on OLED screens. SwiftUI simplifies this process with its adaptive color system and easy-to-use APIs.

Why Implement Dark Mode?

  • Improved User Experience: Reduces eye strain in low-light conditions.
  • Accessibility: Benefits users with light sensitivity.
  • Battery Saving: OLED screens consume less power in Dark Mode.
  • Modern Aesthetic: Provides a sleek and contemporary look.

How to Implement Dark Mode Support in SwiftUI

Step 1: Use Adaptive Colors

SwiftUI’s built-in color system automatically adapts to the current appearance mode (Light or Dark). Instead of hardcoding colors, use system colors provided by Color and UIColor (via UIColor.init(dynamicProvider:)).

Example:
import SwiftUI

struct ContentView: View {
    var body: some View {
        ZStack {
            Color.primary.edgesIgnoringSafeArea(.all) // Adapts to light and dark mode

            VStack {
                Text("Hello, Dark Mode!")
                    .foregroundColor(Color.secondary) // Also adapts
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
            .preferredColorScheme(.dark) // Forcing dark mode preview
    }
}

Step 2: Customize Colors for Dark Mode

Sometimes, you may need more control over specific color adaptations. You can define custom colors that change based on the current appearance.

Method 1: Using Assets Catalog

The recommended approach is to use the Assets Catalog:

  1. Open Assets.xcassets.
  2. Click the “+” button and select “New Color Set”.
  3. Name the color set (e.g., CustomBackgroundColor).
  4. In the Attributes Inspector, find the “Appearances” setting and set it to “Any, Dark”.
  5. Define the colors for both Any Appearance and Dark Appearance.

Then, use this color in your SwiftUI view:

struct ContentView: View {
    var body: some View {
        ZStack {
            Color("CustomBackgroundColor").edgesIgnoringSafeArea(.all)

            VStack {
                Text("Hello, Dark Mode!")
                    .foregroundColor(Color.white) // Or any suitable color
            }
        }
    }
}
Method 2: Using UIColor(dynamicProvider:)

For more complex color logic or when working programmatically, you can use UIColor.init(dynamicProvider:):

struct ContentView: View {
    var backgroundColor: Color {
        Color(UIColor { traitCollection in
            if traitCollection.userInterfaceStyle == .dark {
                return UIColor.black // Dark mode color
            } else {
                return UIColor.white // Light mode color
            }
        })
    }

    var body: some View {
        ZStack {
            backgroundColor.edgesIgnoringSafeArea(.all)

            VStack {
                Text("Hello, Dark Mode!")
                    .foregroundColor(.white) // Or any suitable color
            }
        }
    }
}

Step 3: Adaptive Images and Icons

Similar to colors, you can provide different versions of images and icons for light and dark modes.

  1. Open Assets.xcassets.
  2. Select the image set.
  3. In the Attributes Inspector, set the “Appearances” to “Any, Dark”.
  4. Drag the light and dark mode images into their respective slots.

In your SwiftUI view, simply use the image name, and it will automatically adapt:

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            Image("MyAdaptiveImage") // Automatically switches based on appearance

            Text("Hello, Dark Mode!")
                .foregroundColor(.primary)
        }
    }
}

Step 4: Handling Dynamic System Fonts

Using dynamic type is essential for accessibility. Ensure your text elements use fonts that respond to the user’s preferred text size, and adjust contrast according to the color scheme.

import SwiftUI

struct ContentView: View {
    var body: some View {
        Text("Dynamic Font Example")
            .font(.system(size: 20, design: .default))
            .scaledToFill()
            .minimumScaleFactor(0.5) // Allows the text to scale down if needed
    }
}

Step 5: Check Current Appearance

In some scenarios, you might need to explicitly check the current appearance to perform different actions or display specific content.

import SwiftUI

struct ContentView: View {
    @Environment(\\.colorScheme) var colorScheme

    var body: some View {
        VStack {
            if colorScheme == .dark {
                Text("Dark Mode Active!")
                    .foregroundColor(.white)
            } else {
                Text("Light Mode Active!")
                    .foregroundColor(.black)
            }
        }
    }
}

Here, @Environment(\\.colorScheme) is used to get the current color scheme.

Step 6: Supporting System-Wide Dark Mode Changes

Your app automatically responds when the system-wide appearance settings change. However, you can observe these changes to update the UI accordingly.

Best Practices

  • Test Thoroughly: Always test your app in both light and dark modes to ensure visual consistency.
  • Accessibility: Verify that color contrasts meet accessibility guidelines.
  • User Preferences: Allow users to override the system-wide setting if needed.

Conclusion

Implementing Dark Mode in SwiftUI is straightforward, thanks to its adaptive color system and flexible APIs. By utilizing system colors, customizing colors through the Assets Catalog or programmatically, and ensuring your images and text adapt accordingly, you can create an application that provides a comfortable and modern experience for all users. Dark mode not only improves usability in different lighting conditions but also contributes to a more polished and user-friendly app.