34

If I have a SwiftUI Color:

let col: Color = Color(red: 0.5, green: 0.5, blue: 0.5)

How do I get the RGB components from col?
Like this maybe:

print(col.components.red)

In UIKit, I could use UIColor.getRed but there doesn't seem to be an equivalent in SwiftUI.

Mojtaba Hosseini
  • 95,414
  • 31
  • 268
  • 278
Div
  • 1,363
  • 2
  • 11
  • 28

7 Answers7

40

iOS 17/ macOS 14 (advanced but native)

You can ask for resolving Color components in the given environment, because colors are different in different environments (for example in dark and light environments). In the following sample, I resolved it using the current environment of the used color.

struct ContentView: View {
    @Environment(\.self) var environment
    @State private var color = Color.red
    @State private var components: Color.Resolved?

    var body: some View {
        VStack {
            ColorPicker("Select your favorite color", selection: $color)

            if let components {
                Text("R: \(components.red)")
                Text("G: \(components.green)")
                Text("B: \(components.blue)")
                Text("A: \(components.opacity)")
                Text("HEX: \(components.description)")
            }
        }
        .padding()
        .onChange(of: color, initial: true) { components = color.resolve(in: environment) }
    }
}

The code above has been written for iOS 17 beta 1 using Xcode 15 beta 1


iOS 14 / macOS 10.16

There is a new initializer that takes a Color and returns a UIColor for iOS or NSColor for macOS now. With the help of those you can implement the following extensions:

import SwiftUI

#if canImport(UIKit)
import UIKit
#elseif canImport(AppKit)
import AppKit
#endif

extension Color {
    var components: (red: CGFloat, green: CGFloat, blue: CGFloat, opacity: CGFloat) {

        #if canImport(UIKit)
        typealias NativeColor = UIColor
        #elseif canImport(AppKit)
        typealias NativeColor = NSColor
        #endif

        var r: CGFloat = 0
        var g: CGFloat = 0
        var b: CGFloat = 0
        var o: CGFloat = 0

        guard NativeColor(self).getRed(&r, green: &g, blue: &b, alpha: &o) else {
            // You can handle the failure here as you want
            return (0, 0, 0, 0)
        }

        return (r, g, b, o)
    }

    var hex: String {
        String(
            format: "#%02x%02x%02x%02x",
            Int(components.red * 255),
            Int(components.green * 255),
            Int(components.blue * 255),
            Int(components.opacity * 255)
        )
    }
}

Usage

Color.red.components.red // 0.9999999403953552 // <- SwiftUI Colors are not pure!
Mojtaba Hosseini
  • 95,414
  • 31
  • 268
  • 278
  • 1
    I found that this somehow loses the dark appearance set in assets? – Kai Zheng Oct 18 '20 at 08:34
  • I've managed to fix this, see answer below. Would be too much for an edit. – Kai Zheng Oct 18 '20 at 09:21
  • If you are using ColorPicker on macOS, you should use NativeColor(self).usingColorSpace(NSColorSpace.extendedSRGB)?.getRed(&r, green: &g, blue: &b, alpha: &o) or else dynamic colors will cause a crash. – Matt Aug 19 '22 at 23:17
10

Simple one-liner:

print(UIColor(Color.blue).cgColor.components)

You get an [CGFloat]? of [red, green, blue, alpha].

marcantonio
  • 958
  • 10
  • 24
6

Waiting for an API I've abused CustomStringConvertible protocol for the simple rgba case where the color description format is #rrggbbaa

debugPrint(Color.red)
debugPrint(Color(red: 1.0, green: 0.0, blue: 0.0))
debugPrint(Color(red: 1.0, green: 0.3, blue: 0.0))
debugPrint(Color(.sRGB, red: 1.0, green: 0.0, blue: 0.5, opacity: 0.3))
debugPrint(Color(hue: 1.0, saturation: 0.0, brightness: 1.0))
debugPrint(Color(.displayP3, red: 1.0, green: 0.0, blue: 0.0, opacity: 1.0).description)

red
#FF0000FF
#FF4C00FF
#FF00804D
#FFFFFFFF
"DisplayP3(red: 1.0, green: 0.0, blue: 0.0, opacity: 1.0)"

as you can see, things like Color.red just dump "red" but if you are working with simple RGB colors generated by code (ie from a color picker) then this is not too bad

extension SwiftUI.Color {
    var redComponent: Double? {
        let val = description
        guard val.hasPrefix("#") else { return nil }
        let r1 = val.index(val.startIndex, offsetBy: 1)
        let r2 = val.index(val.startIndex, offsetBy: 2)
        return Double(Int(val[r1...r2], radix: 16)!) / 255.0
    }

    var greenComponent: Double? {
        let val = description
        guard val.hasPrefix("#") else { return nil }
        let g1 = val.index(val.startIndex, offsetBy: 3)
        let g2 = val.index(val.startIndex, offsetBy: 4)
        return Double(Int(val[g1...g2], radix: 16)!) / 255.0
    }

    var blueComponent: Double? {
        let val = description
        guard val.hasPrefix("#") else { return nil }
        let b1 = val.index(val.startIndex, offsetBy: 5)
        let b2 = val.index(val.startIndex, offsetBy: 6)
        return Double(Int(val[b1...b2], radix: 16)!) / 255.0
    }

    var opacityComponent: Double? {
        let val = description
        guard val.hasPrefix("#") else { return nil }
        let b1 = val.index(val.startIndex, offsetBy: 7)
        let b2 = val.index(val.startIndex, offsetBy: 8)
        return Double(Int(val[b1...b2], radix: 16)!) / 255.0
    }
}
Nicola Ferruzzi
  • 368
  • 4
  • 6
3

I have found that @Mojtaba Hosseinis answer is working fine, except when you have your colors declared inside assets with light and dark appearances.

Then I found that the dark appearance somehow gets lost when using UIColor(self). Here is a workaround I came up with:

Note, this is only for iOS since my app is iOS only, you could of course do the same as @Mojtaba Hosseini and adapt it to macOS as well.

extension Color {

    var components: (r: Double, g: Double, b: Double, o: Double)? {
        let uiColor: UIColor
        
        var r: CGFloat = 0
        var g: CGFloat = 0
        var b: CGFloat = 0
        var o: CGFloat = 0
        
        if self.description.contains("NamedColor") {
            let lowerBound = self.description.range(of: "name: \"")!.upperBound
            let upperBound = self.description.range(of: "\", bundle")!.lowerBound
            let assetsName = String(self.description[lowerBound..<upperBound])
            
            uiColor = UIColor(named: assetsName)!
        } else {
            uiColor = UIColor(self)
        }

        guard uiColor.getRed(&r, green: &g, blue: &b, alpha: &o) else { return nil }
        
        return (Double(r), Double(g), Double(b), Double(o))
    }
}

The idea is to use the UIColor(named:) initializer instead, where all appearances are correct. Fortunately, the name we set in assets is saved in the description of the Color. We only have to abstract it since there is also other information, namely bundle, etc.

Kai Zheng
  • 6,640
  • 7
  • 43
  • 66
2

Based on @Mojtaba's answer, I came up with a shorter, more flexible version:

#if canImport(UIKit)
import UIKit
#elseif canImport(AppKit)
import AppKit
#endif

extension Color {
    #if canImport(UIKit)
    var asNative: UIColor { UIColor(self) }
    #elseif canImport(AppKit)
    var asNative: NSColor { NSColor(self) }
    #endif

    var rgba: (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) {
        let color = asNative.usingColorSpace(.deviceRGB)!
        var t = (CGFloat(), CGFloat(), CGFloat(), CGFloat())
        color.getRed(&t.0, green: &t.1, blue: &t.2, alpha: &t.3)
        return t
    }

    var hsva: (hue: CGFloat, saturation: CGFloat, value: CGFloat, alpha: CGFloat) {
        let color = asNative.usingColorSpace(.deviceRGB)!
        var t = (CGFloat(), CGFloat(), CGFloat(), CGFloat())
        color.getHue(&t.0, saturation: &t.1, brightness: &t.2, alpha: &t.3)
        return t
    }
}

Doing asNative.redComponent etc. might also work, FYI.

Ivorius
  • 356
  • 2
  • 4
  • 13
1

The answer is no - there's no API do so (yet), but...

Most of SwiftUI structs have fields that are private, like in Color.

You can use Mirror to extract such informations - but keep in mind it is not efficient.

Here's how to extract the hexadecimal representation of a SwiftUI Color - for educational purpose.

Copy and paste this into a Xcode 11 playground.

import UIKit
import SwiftUI

let systemColor = Color.red
let color = Color(red: 0.3, green: 0.5, blue: 1)

extension Color {

    var hexRepresentation: String? {
        let children = Mirror(reflecting: color).children
        let _provider = children.filter { $0.label == "provider" }.first
        guard let provider = _provider?.value else {
            return nil
        }
        let providerChildren = Mirror(reflecting: provider).children
        let _base = providerChildren.filter { $0.label == "base" }.first
        guard let base = _base?.value else {
            return nil
        }
        var baseValue: String = ""
        dump(base, to: &baseValue)
        guard let firstLine = baseValue.split(separator: "\n").first,
              let hexString = firstLine.split(separator: " ")[1] as Substring? else {
            return nil
        }
        return hexString.trimmingCharacters(in: .newlines)
    }

}

systemColor.hexRepresentation
color.hexRepresentation

Colors like .red, .white, etc., don't seem to have many information in them, when dumped.

Just their "system" name.

▿ red
  ▿ provider: SwiftUI.(unknown context at $1297483bc).ColorBox<SwiftUI.SystemColorType> #0
    - super: SwiftUI.(unknown context at $129748300).AnyColorBox
    - base: SwiftUI.SystemColorType.red

A Color instantiated with red/blue/green components does instead.

▿ #4C80FFFF
  ▿ provider: SwiftUI.(unknown context at $11cd2e3bc).ColorBox<SwiftUI.Color._Resolved> #0
    - super: SwiftUI.(unknown context at $11cd2e300).AnyColorBox
    ▿ base: #4C80FFFF
      - linearRed: 0.073238954
      - linearGreen: 0.21404114
      - linearBlue: 1.0
      - opacity: 1.0

In the Playground, you will see:

  • systemColor.hexRepresentation returning nil
  • color.hexRepresentation returning "#4C80FFFF"
Matteo Pacini
  • 21,796
  • 7
  • 67
  • 74
  • 5
    I feel like the answer to this question should be “you don’t”. Data flow in SwiftUI goes from the data into the views. You don’t get data out of the views. You put data into the views. If you have a colour then it only exists because you gave it the rgb values. So you already have the data. – Fogmeister Jun 13 '19 at 19:51
  • 4
    @Fogmeister Good point - but it's always good fun digging into private implementations :) – Matteo Pacini Jun 13 '19 at 19:54
  • Hehe nice find :) – J. Doe Jun 13 '19 at 22:01
  • 1
    @Fogmeister I agree with the general sentiment, but how would you suggest I store color in my code? Say I have a model for a "Page" that has a background color. Should I create my own Color struct, then convert to SwiftUI color in the view? This seems like something a framework should provide... – Hannes Hertach Jul 24 '20 at 14:51
1

You can use UIColor and transform the UIColor to Color after. Code:

extension UIColor {
    func hexValue() -> String {
        let values = self.cgColor.components
        var outputR: Int = 0
        var outputG: Int = 0
        var outputB: Int = 0
        var outputA: Int = 1

        switch values!.count {
            case 1:
                outputR = Int(values![0] * 255)
                outputG = Int(values![0] * 255)
                outputB = Int(values![0] * 255)
                outputA = 1
            case 2:
                outputR = Int(values![0] * 255)
                outputG = Int(values![0] * 255)
                outputB = Int(values![0] * 255)
                outputA = Int(values![1] * 255)
            case 3:
                outputR = Int(values![0] * 255)
                outputG = Int(values![1] * 255)
                outputB = Int(values![2] * 255)
                outputA = 1
            case 4:
                outputR = Int(values![0] * 255)
                outputG = Int(values![1] * 255)
                outputB = Int(values![2] * 255)
                outputA = Int(values![3] * 255)
            default:
                break
        }
        return "#" + String(format:"%02X", outputR) + String(format:"%02X", outputG) + String(format:"%02X", outputB) + String(format:"%02X", outputA)
    }
}