Dark mode support and adaptive colors

Sofia Martinez Jan 2026
1 tab
import SwiftUI

// MARK: - Semantic Colors in SwiftUI
struct AdaptiveColorsView: View {
    var body: some View {
        VStack(spacing: 20) {
            Text("Primary Label")
                .foregroundColor(.primary)

            Text("Secondary Label")
                .foregroundColor(.secondary)

            Rectangle()
                .fill(Color(.systemBackground))
                .frame(height: 100)
                .overlay(
                    Text("System Background")
                )

            Rectangle()
                .fill(Color(.secondarySystemBackground))
                .frame(height: 100)
                .overlay(
                    Text("Secondary Background")
                )
        }
        .padding()
    }
}

// MARK: - Custom Adaptive Colors
extension Color {
    static let customPrimary = Color("CustomPrimary")
    static let customAccent = Color("CustomAccent")

    static func adaptive(light: Color, dark: Color) -> Color {
        Color(UIColor { traitCollection in
            traitCollection.userInterfaceStyle == .dark ? UIColor(dark) : UIColor(light)
        })
    }
}

// MARK: - UIKit Dark Mode
extension UIColor {
    static let customBackground = UIColor { traitCollection in
        traitCollection.userInterfaceStyle == .dark ?
            UIColor(red: 0.1, green: 0.1, blue: 0.1, alpha: 1) :
            UIColor(red: 0.95, green: 0.95, blue: 0.95, alpha: 1)
    }

    static let customText = UIColor { traitCollection in
        traitCollection.userInterfaceStyle == .dark ?
            .white :
            .black
    }
}

// MARK: - Respond to Appearance Changes
class ThemeManager: ObservableObject {
    @Published var currentTheme: Theme = .system

    enum Theme: String, CaseIterable {
        case light = "Light"
        case dark = "Dark"
        case system = "System"

        var colorScheme: ColorScheme? {
            switch self {
            case .light: return .light
            case .dark: return .dark
            case .system: return nil
            }
        }
    }

    func apply(_ theme: Theme) {
        currentTheme = theme
        UserDefaults.standard.set(theme.rawValue, forKey: "selectedTheme")
    }
}

struct ThemedView: View {
    @StateObject private var themeManager = ThemeManager()
    @Environment(\.colorScheme) var systemColorScheme

    var body: some View {
        NavigationView {
            VStack(spacing: 20) {
                Text("Current Theme")
                    .font(.title)

                Picker("Theme", selection: $themeManager.currentTheme) {
                    ForEach(ThemeManager.Theme.allCases, id: \.self) { theme in
                        Text(theme.rawValue).tag(theme)
                    }
                }
                .pickerStyle(.segmented)

                VStack {
                    Text("Sample Content")
                        .foregroundColor(.primary)
                    Text("Secondary Text")
                        .foregroundColor(.secondary)
                }
                .padding()
                .background(Color(.secondarySystemBackground))
                .cornerRadius(10)
            }
            .padding()
            .navigationTitle("Theme Settings")
        }
        .preferredColorScheme(themeManager.currentTheme.colorScheme)
    }
}

// MARK: - UIKit View Controller
class DarkModeViewController: UIViewController {
    private let label = UILabel()
    private let backgroundView = UIView()

    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
    }

    private func setupUI() {
        view.backgroundColor = .systemBackground

        backgroundView.backgroundColor = .customBackground
        backgroundView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(backgroundView)

        label.text = "Adaptive Content"
        label.textColor = .customText
        label.font = .systemFont(ofSize: 20, weight: .medium)
        label.translatesAutoresizingMaskIntoConstraints = false
        backgroundView.addSubview(label)

        NSLayoutConstraint.activate([
            backgroundView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            backgroundView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            backgroundView.widthAnchor.constraint(equalToConstant: 300),
            backgroundView.heightAnchor.constraint(equalToConstant: 200),

            label.centerXAnchor.constraint(equalTo: backgroundView.centerXAnchor),
            label.centerYAnchor.constraint(equalTo: backgroundView.centerYAnchor)
        ])
    }

    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        super.traitCollectionDidChange(previousTraitCollection)

        if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) {
            // Appearance changed
            updateAppearance()
        }
    }

    private func updateAppearance() {
        // Refresh UI elements that need manual updates
        label.textColor = .customText
        backgroundView.backgroundColor = .customBackground
    }
}
1 file · swift Explain with highlit

Dark mode provides low-light interface that reduces eye strain and saves battery on OLED displays. iOS automatically switches based on system settings or user preference. I use semantic colors like .label, .secondaryLabel, and .systemBackground that adapt automatically. For custom colors, I create color assets in Asset Catalog with light and dark variants. UIColor(named:) loads adaptive colors. In SwiftUI, Color initializers like Color("MyColor") work similarly. I test both modes using environment overrides or Xcode's appearance selector. Images can have dark variants using asset catalogs. The .preferredColorScheme() modifier forces a specific mode. Respecting user's appearance choice is important for accessibility and battery life.