Search code examples
swiftui

Wrong dark mode color accent


When I retrieve the accent color programmatically, it returns the accent color in the light mode. However, when I retrieve the color alone, it returns the correct color. How can I resolve this issue?

import SwiftUI

extension Color {
    // Function to get RGB components from Color
    private func getRGBA() -> (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) {
        let uiColor = UIColor(self)
        var red: CGFloat = 0
        var green: CGFloat = 0
        var blue: CGFloat = 0
        var alpha: CGFloat = 0
        uiColor.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
        return (red, green, blue, alpha)
    }

    // Generate array of colors between two colors
    static func colorRange(from startColor: Color, to endColor: Color, count: Int) -> [Color] {
        guard count > 1 else { return [startColor] }

        let start = startColor.getRGBA()
        let end = endColor.getRGBA()

        let redDelta = end.red - start.red
        let greenDelta = end.green - start.green
        let blueDelta = end.blue - start.blue
        let alphaDelta = end.alpha - start.alpha

        return (0 ..< count).map { step in
            let progress = CGFloat(step) / CGFloat(count - 1)

            let red = start.red + redDelta * progress
            let green = start.green + greenDelta * progress
            let blue = start.blue + blueDelta * progress
            let alpha = start.alpha + alphaDelta * progress

            return Color(UIColor(red: red, green: green, blue: blue, alpha: alpha))
        }
    }
}

struct ColorRangeExamples: View {
    var body: some View {
        VStack(spacing: 30) {
            Color.accent.frame(height: 100).cornerRadius(10)
            
            // Example 1: 5 colors between accent and secondary
            let example1 = Color.colorRange(from: .accent, to: .secondary, count: 5)
            HStack(spacing: 0) {
                ForEach(0 ..< example1.count, id: \.self) { index in
                    example1[index]
                }
            }
            .frame(height: 50)
            .cornerRadius(10)
        }
        .padding()
    }
}

#Preview {
    ColorRangeExamples()
}

Preview in Light and Dark mode


Solution

  • UIColor.getRGBA does not respect the current color scheme.

    You should use resolve(in:) to get the RGBA values. This method takes an EnvironmentValues which includes the color scheme information. It gives you a Color.Resolved, from which you can directly get the RGBA values.

    extension Color {
        func getRGBA(env: EnvironmentValues) -> (red: Float, green: Float, blue: Float, alpha: Float) {
            let resolved = resolve(in: env)
            return (resolved.red, resolved.green, resolved.blue, resolved.opacity)
        }
    }
    

    Similarly, colorRange needs to take such an EnvironmentValues parameter too.

    static func colorRange(from startColor: Color, to endColor: Color, count: Int, env: EnvironmentValues) -> [Color.Resolved] {
        guard count > 1 else { return [startColor.resolve(in: env)] }
    
        let start = startColor.getRGBA(env: env)
        let end = endColor.getRGBA(env: env)
    
        let redDelta = end.red - start.red
        let greenDelta = end.green - start.green
        let blueDelta = end.blue - start.blue
        let alphaDelta = end.alpha - start.alpha
    
        return (0 ..< count).map { step in
            let progress = Float(step) / Float(count - 1)
    
            let red = start.red + redDelta * progress
            let green = start.green + greenDelta * progress
            let blue = start.blue + blueDelta * progress
            let alpha = start.alpha + alphaDelta * progress
    
            return Color.Resolved(red: red, green: green, blue: blue, opacity: alpha)
        }
    }
    

    In your view, use @Environment(\.self) to get the EnvironmentValues.

    struct ColorRangeExamples: View {
        @Environment(\.self) var env
        
        var body: some View {
            VStack(spacing: 30) {
                Color.accent.frame(height: 100).cornerRadius(10)
                
                // Example 1: 5 colors between accent and secondary
                let example1 = Color.colorRange(from: .accent, to: .secondary, count: 5, env: env)
                HStack(spacing: 0) {
                    ForEach(0 ..< example1.count, id: \.self) { index in
                        Color(example1[index])
                    }
                }
                .frame(height: 50)
                .cornerRadius(10)
            }
            .padding()
        }
    }
    

    If you need to support earlier versions, you can use UIColor.resolvedColor(with:), which is similar to Color.resolve(in:), except it takes a UITraitCollection. You need to implement a UIViewRepresentable in order to get a UITraitCollection in SwiftUI.

    extension Color {
        private func getRGBA(traits: UITraitCollection) -> (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) {
            let uiColor = UIColor(self).resolvedColor(with: traits)
            var red: CGFloat = 0
            var green: CGFloat = 0
            var blue: CGFloat = 0
            var alpha: CGFloat = 0
            uiColor.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
            return (red, green, blue, alpha)
        }
    
        static func colorRange(from startColor: Color, to endColor: Color, count: Int, traits: UITraitCollection) -> [Color] {
            guard count > 1 else { return [startColor] }
    
            let start = startColor.getRGBA(traits: traits)
            let end = endColor.getRGBA(traits: traits)
    
            let redDelta = end.red - start.red
            let greenDelta = end.green - start.green
            let blueDelta = end.blue - start.blue
            let alphaDelta = end.alpha - start.alpha
    
            return (0 ..< count).map { step in
                let progress = CGFloat(step) / CGFloat(count - 1)
    
                let red = start.red + redDelta * progress
                let green = start.green + greenDelta * progress
                let blue = start.blue + blueDelta * progress
                let alpha = start.alpha + alphaDelta * progress
    
                return Color(UIColor(red: red, green: green, blue: blue, alpha: alpha))
            }
        }
    }
    
    struct TraitsReader<Content: View>: View {
        class TraitObserver: UIView {
            var traitsDidChange: ((UITraitCollection) -> Void)?
    
            // note that this method is deprecated in iOS 17.0
            override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
                DispatchQueue.main.async {
                    self.traitsDidChange?(self.traitCollection)
                }
            }
        }
        
        struct TraitsAccessor: UIViewRepresentable {
            @Binding var traits: UITraitCollection
            
            func makeUIView(context: Context) -> TraitObserver {
                TraitObserver()
            }
            
            func updateUIView(_ uiView: TraitObserver, context: Context) {
                uiView.traitsDidChange = { traits = $0 }
            }
        }
        
        @ViewBuilder let content: (UITraitCollection) -> Content
        @State var traits: UITraitCollection = .init()
        
        var body: some View {
            content(traits).background {
                TraitsAccessor(traits: $traits)
            }
        }
    }
    
    struct ColorRangeExamples: View {
        var body: some View {
            TraitsReader { traits in
                VStack(spacing: 30) {
                    Color.accent.frame(height: 100).cornerRadius(10)
                    
                    // Example 1: 5 colors between accent and secondary
                    let example1 = Color.colorRange(from: .accent, to: .secondary, count: 5, traits: traits)
                    HStack(spacing: 0) {
                        ForEach(0 ..< example1.count, id: \.self) { index in
                            example1[index]
                        }
                    }
                    .frame(height: 50)
                    .cornerRadius(10)
                }
                .padding()
            }
        }
    }