77

I'm trying to change a SwiftUI Color to an instance of UIColor.

I can easily get the RGBA from the UIColor, but I don't know how to get the "Color" instance to return the corresponding RGB and opacity values.

@EnvironmentObject var colorStore: ColorStore

init() {
    let red =     //get red value from colorStore.primaryThemeColor
    let green = //get green value from colorStore.primaryThemeColor
    let blue =   //get blue value from colorStore.primaryThemeColor
    let alpha = //get alpha value from colorStore.primaryThemeColor
    
    let color = UIColor(red: red, green: green, blue: blue, alpha: alpha)
    UINavigationBar.appearance().tintColor = color
}

...or maybe there is a better way to accomplish what I am looking for?

pkamb
  • 33,281
  • 23
  • 160
  • 191
Gavin Jensen
  • 1,371
  • 2
  • 9
  • 14

7 Answers7

107

SwiftUI 2.0

There is a new initializer that takes a Color and returns a UIColor for iOS or NSColor for macOS now. So:

iOS

UIColor(Color.red)

macOS

NSColor(Color.red)

Core Graphics

UIColor(Color.red).cgColor /* For iOS */
NSColor(Color.red).cgColor /* For macOS */

If you are looking for color components, you can find a helpful extensions here in this answer

Also, check out How to convert UIColor to SwiftUI‘s Color

Mojtaba Hosseini
  • 95,414
  • 31
  • 268
  • 278
  • the core graphics seems to be wrong. Doing exactly that returns "Cannot invoke initializer for type 'CGColor' with an argument list of type '(Color)'" – Jandro Rojas Sep 16 '20 at 20:24
  • `.cgColor` exists on `Color` from iOS 14. https://developer.apple.com/documentation/swiftui/color/cgcolor – Patrick Aug 06 '21 at 19:41
34

How about this solution?

extension Color {
 
    func uiColor() -> UIColor {

        if #available(iOS 14.0, *) {
            return UIColor(self)
        }

        let components = self.components()
        return UIColor(red: components.r, green: components.g, blue: components.b, alpha: components.a)
    }

    private func components() -> (r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat) {

        let scanner = Scanner(string: self.description.trimmingCharacters(in: CharacterSet.alphanumerics.inverted))
        var hexNumber: UInt64 = 0
        var r: CGFloat = 0.0, g: CGFloat = 0.0, b: CGFloat = 0.0, a: CGFloat = 0.0

        let result = scanner.scanHexInt64(&hexNumber)
        if result {
            r = CGFloat((hexNumber & 0xff000000) >> 24) / 255
            g = CGFloat((hexNumber & 0x00ff0000) >> 16) / 255
            b = CGFloat((hexNumber & 0x0000ff00) >> 8) / 255
            a = CGFloat(hexNumber & 0x000000ff) / 255
        }
        return (r, g, b, a)
    }
}

Usage:

let uiColor = myColor.uiColor()

It's a bit of a hack, but it's at least something until we get a valid method for this. The key here is self.description which gives a hexadecimal description of the color (if it's not dynamic I should add). And the rest is just calculations to get the color components, and create a UIColor.

lewis
  • 2,936
  • 2
  • 37
  • 72
turingtested
  • 6,356
  • 7
  • 32
  • 47
  • This may be hacky, but it's the correct answer in my opinion. There are definitely reasons for wanting to interrogate the Color type. For example, I have an extension on Color that returns an appropriate text color for the given Color as a background. When all I have to work with to begin with is the *var accentColor: Color*, then I need to be able to interrogate the makeup of that Color. Upvoted for creativity alone and I hope that Apple has more to add to this next year. – David Monagle Nov 02 '19 at 00:17
  • 9
    NB: this does not work if you're referencing a color in an asset catalog (where `.description` yields `"NamedColor(name: \"Dark Roasts/Espresso100\", bundle: nil)"` or similar) – sam-w Jan 09 '20 at 05:37
  • 3
    this does not work in case the color is `.red`, the `result` variable returns false in this case but I don't know why. – JAHelia May 12 '20 at 13:52
  • 1
    @JAHelia This is because `Color.red` is dynamic, i.e. it changes depending on the current mode. When dark mode is on, the red also gets darker. So it has no single value, that's why `result` returns false. – turingtested May 12 '20 at 15:15
  • This isn't working. let components = self.components() always returns (0,0,0,0) – Tharindu Ketipearachchi Jan 07 '21 at 06:25
8

Currently, this is not directly made available in the SwiftUI API. However, I managed to get a makeshift initializer working that utilizes debug prints and dump. I found that all of the other solutions failed to account for a Color initialized from a name, a bundle, the .displayP3 color space, a UIColor, a static system Color, or any color whose opacity altered. My solution accounts for all of the aforementioned downfalls.

fileprivate struct ColorConversionError: Swift.Error {
    let reason: String
}

extension Color {

    @available(*, deprecated, message: "This is fragile and likely to break at some point. Hopefully it won't be required for long.")
    var uiColor: UIColor {
        do {
            return try convertToUIColor()
        } catch let error {
            assertionFailure((error as! ColorConversionError).reason)
            return .black
        }
    }
}

fileprivate extension Color {

    var stringRepresentation: String { description.trimmingCharacters(in: .whitespacesAndNewlines) }
    var internalType: String { "\(type(of: Mirror(reflecting: self).children.first!.value))".replacingOccurrences(of: "ColorBox<(.+)>", with: "$1", options: .regularExpression) }

    func convertToUIColor() throws -> UIColor  {
        if let color = try OpacityColor(color: self) {
            return try UIColor.from(swiftUIDescription: color.stringRepresentation, internalType: color.internalType).multiplyingAlphaComponent(by: color.opacityModifier)
        }
        return try UIColor.from(swiftUIDescription: stringRepresentation, internalType: internalType)
    }
}

fileprivate struct OpacityColor {

    let stringRepresentation: String
    let internalType: String
    let opacityModifier: CGFloat

    init(stringRepresentation: String, internalType: String, opacityModifier: CGFloat) {
        self.stringRepresentation = stringRepresentation
        self.internalType = internalType
        self.opacityModifier = opacityModifier
    }

    init?(color: Color) throws {
        guard color.internalType == "OpacityColor" else {
            return nil
        }
        let string = color.stringRepresentation

        let opacityRegex = try! NSRegularExpression(pattern: #"(\d+% )"#)
        let opacityLayerCount = opacityRegex.numberOfMatches(in: string, options: [], range: NSRange(string.startIndex..<string.endIndex, in: string))
        var dumpStr = ""
        dump(color, to: &dumpStr)
        dumpStr = dumpStr.replacingOccurrences(of: #"^(?:.*\n){\#(4 * opacityLayerCount)}.*?base: "#, with: "", options: .regularExpression)

        let opacityModifier = dumpStr.split(separator: "\n")
            .suffix(1)
            .lazy
            .map { $0.replacingOccurrences(of: #"\s+-\s+opacity: "#, with: "", options: .regularExpression) }
            .map { CGFloat(Double($0)!) }
            .reduce(1, *)

        let internalTypeRegex = try! NSRegularExpression(pattern: #"^.*\n.*ColorBox<.*?([A-Za-z0-9]+)>"#)
        let matches = internalTypeRegex.matches(in: dumpStr, options: [], range: NSRange(dumpStr.startIndex..<dumpStr.endIndex, in: dumpStr))
        guard let match = matches.first, matches.count == 1, match.numberOfRanges == 2 else {
            throw ColorConversionError(reason: "Could not parse internalType from \"\(dumpStr)\"")
            try! self.init(color: Color.black.opacity(1))
        }

        self.init(
            stringRepresentation: String(dumpStr.prefix { !$0.isNewline }),
            internalType: String(dumpStr[Range(match.range(at: 1), in: dumpStr)!]),
            opacityModifier: opacityModifier
        )
    }
}

fileprivate extension UIColor {

    static func from(swiftUIDescription description: String, internalType: String) throws -> UIColor {
        switch internalType {
        case "SystemColorType":
            guard let uiColor = UIColor.from(systemColorName: description) else {
                throw ColorConversionError(reason: "Could not parse SystemColorType from \"\(description)\"")
            }

            return uiColor

        case "_Resolved":
            guard description.range(of: "^#[0-9A-F]{8}$", options: .regularExpression) != nil else {
                throw ColorConversionError(reason: "Could not parse hex from \"\(description)\"")
            }

            let components = description
                .dropFirst()
                .chunks(of: 2)
                .compactMap { CGFloat.decimalFromHexPair(String($0)) }

            guard components.count == 4, let cgColor = CGColor(colorSpace: CGColorSpace(name: CGColorSpace.linearSRGB)!, components: components) else {
                throw ColorConversionError(reason: "Could not parse hex from \"\(description)\"")
            }

            return UIColor(cgColor: cgColor)

        case "UIColor":
            let sections = description.split(separator: " ")
            let colorSpace = String(sections[0])
            let components = sections[1...]
                .compactMap { Double($0) }
                .map { CGFloat($0) }

            guard components.count == 4 else {
                throw ColorConversionError(reason: "Could not parse UIColor components from \"\(description)\"")
            }
            let (r, g, b, a) = (components[0], components[1], components[2], components[3])
            return try UIColor(red: r, green: g, blue: b, alpha: a, colorSpace: colorSpace)

        case "DisplayP3":
            let regex = try! NSRegularExpression(pattern: #"^DisplayP3\(red: (-?\d+(?:\.\d+)?), green: (-?\d+(?:\.\d+)?), blue: (-?\d+(?:\.\d+)?), opacity: (-?\d+(?:\.\d+)?)"#)
            let matches = regex.matches(in: description, options: [], range: NSRange(description.startIndex..<description.endIndex, in: description))
            guard let match = matches.first, matches.count == 1, match.numberOfRanges == 5 else {
                throw ColorConversionError(reason: "Could not parse DisplayP3 from \"\(description)\"")
            }

            let components = (0..<match.numberOfRanges)
                .dropFirst()
                .map { Range(match.range(at: $0), in: description)! }
                .compactMap { Double(String(description[$0])) }
                .map { CGFloat($0) }

            guard components.count == 4 else {
                throw ColorConversionError(reason: "Could not parse DisplayP3 components from \"\(description)\"")
            }

            let (r, g, b, a) = (components[0], components[1], components[2], components[3])
            return UIColor(displayP3Red: r, green: g, blue: b, alpha: a)

        case "NamedColor":
            guard description.range(of: #"^NamedColor\(name: "(.*)", bundle: .*\)$"#, options: .regularExpression) != nil else {
                throw ColorConversionError(reason: "Could not parse NamedColor from \"\(description)\"")
            }

            let nameRegex = try! NSRegularExpression(pattern: #"name: "(.*)""#)
            let name = nameRegex.matches(in: description, options: [], range: NSRange(description.startIndex..<description.endIndex, in: description))
                .first
                .flatMap { Range($0.range(at: 1), in: description) }
                .map { String(description[$0]) }

            guard let colorName = name else {
                throw ColorConversionError(reason: "Could not parse NamedColor name from \"\(description)\"")
            }

            let bundleRegex = try! NSRegularExpression(pattern: #"bundle: .*NSBundle <(.*)>"#)
            let bundlePath = bundleRegex.matches(in: description, options: [], range: NSRange(description.startIndex..<description.endIndex, in: description))
                .first
                .flatMap { Range($0.range(at: 1), in: description) }
                .map { String(description[$0]) }
            let bundle =  bundlePath.map { Bundle(path: $0)! }

            return UIColor(named: colorName, in: bundle, compatibleWith: nil)!

        default:
            throw ColorConversionError(reason: "Unhandled type \"\(internalType)\"")
        }
    }

    static func from(systemColorName: String) -> UIColor? {
        switch systemColorName {
        case "clear": return .clear
        case "black": return .black
        case "white": return .white
        case "gray": return .systemGray
        case "red": return .systemRed
        case "green": return .systemGreen
        case "blue": return .systemBlue
        case "orange": return .systemOrange
        case "yellow": return .systemYellow
        case "pink": return .systemPink
        case "purple": return .systemPurple
        case "primary": return .label
        case "secondary": return .secondaryLabel
        default: return nil
        }
    }

    convenience init(red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat, colorSpace: String) throws {
        if colorSpace == "UIDisplayP3ColorSpace" {
            self.init(displayP3Red: red, green: green, blue: blue, alpha: alpha)
        } else if colorSpace == "UIExtendedSRGBColorSpace" {
            self.init(red: red, green: green, blue: blue, alpha: alpha)
        } else if colorSpace == "kCGColorSpaceModelRGB" {
            let colorSpace = CGColorSpace(name: CGColorSpace.linearSRGB)!
            let components = [red, green, blue, alpha]
            let cgColor = CGColor(colorSpace: colorSpace, components: components)!
            self.init(cgColor: cgColor)
        } else {
            throw ColorConversionError(reason: "Unhandled colorSpace \"\(colorSpace)\"")
        }
    }

    func multiplyingAlphaComponent(by multiplier: CGFloat?) -> UIColor {
        var a: CGFloat = 0
        getWhite(nil, alpha: &a)
        return withAlphaComponent(a * (multiplier ?? 1))
    }
}


// MARK: Helper extensions

extension StringProtocol {

    func chunks(of size: Int) -> [Self.SubSequence] {
        stride(from: 0, to: count, by: size).map {
            let start = index(startIndex, offsetBy: $0)
            let end = index(start, offsetBy: size, limitedBy: endIndex) ?? endIndex
            return self[start..<end]
        }
    }
}

extension Int {

    init?(hexString: String) {
        self.init(hexString, radix: 16)
    }
}

extension FloatingPoint {

    static func decimalFromHexPair(_ hexPair: String) -> Self? {
        guard hexPair.count == 2, let value = Int(hexString: hexPair) else {
            return nil
        }
        return Self(value) / Self(255)
    }
}

Note: While this is not a long-term solution for the problem at hand since it hinges on the implementation details of Color which may change at some point, it should work in the interim for most, if not all colors.

sam-w
  • 7,478
  • 1
  • 47
  • 77
Noah Wilder
  • 1,656
  • 20
  • 38
7

@turingtested Updated your answer to get rid of the long tuple crash.

extension Color {
    func uiColor() -> UIColor {
        if #available(iOS 14.0, *) {
            return UIColor(self)
        }

        let scanner = Scanner(string: description.trimmingCharacters(in: CharacterSet.alphanumerics.inverted))
        var hexNumber: UInt64 = 0
        var r: CGFloat = 0.0, g: CGFloat = 0.0, b: CGFloat = 0.0, a: CGFloat = 0.0

        let result = scanner.scanHexInt64(&hexNumber)
        if result {
            r = CGFloat((hexNumber & 0xFF000000) >> 24) / 255
            g = CGFloat((hexNumber & 0x00FF0000) >> 16) / 255
            b = CGFloat((hexNumber & 0x0000FF00) >> 8) / 255
            a = CGFloat(hexNumber & 0x000000FF) / 255
        }
        return UIColor(red: r, green: g, blue: b, alpha: a)
    }
}
Stritt
  • 527
  • 7
  • 7
3

Xcode 14.0, Swift 5.7

Since SwiftUI's Color is a struct, you can extend it with a static function that returns UIColor.

extension Color {
    
    static func convert(_ color: Color) -> UIColor {
        return UIColor(color)
    }
}

In macOS, use a type method with Cocoa's NSColor return:

extension Color {
    
    static func convert(_ color: Color) -> NSColor {
        return NSColor(color)
    }
}

Let's test this method on the BG color of the SpriteKit's scene.

import SwiftUI
import SpriteKit

struct ContentView: View {
    
    @State private var color: Color = .secondary
    let scene = SKScene()
    
    var body: some View {
        let _ = scene.backgroundColor = Color.convert(color)
        VStack {
            SpriteView(scene: scene)
            Rectangle().foregroundColor(color)
        }
    }
}

It's obvious that two grey tints (Color vs UIColor) will be different.

Andy Jazz
  • 49,178
  • 17
  • 136
  • 220
-1

Correct method to create custom UIcolor with color set

init() {
    UISegmentedControl.appearance().selectedSegmentTintColor = UIColor.init(Color("orange"));       
    UISegmentedControl.appearance().setTitleTextAttributes([.foregroundColor: UIColor.black], for: .normal)
    UISegmentedControl.appearance().setTitleTextAttributes([.foregroundColor: UIColor.white], for: .selected)
}
-4

This is not how SwiftUI works. What you are trying to do is very UIKit like. In SwiftUI you rarely interrogate a view for any parameter. At this time, Color does not have any method or properties that return its RGB values. And I doubt there will ever be.

In general, with SwiftUI you need to go to the source, that is, the variable you used to create the color in the first place. For example:

  let r = 0.9
  let g = 0.4
  let b = 0.7
  let mycolor = Color(red: r, green: g, b, opacity: o)

There is nothing like this:

let green = mycolor.greenComponent()

Instead, you need to check variable g (the variable you used to create the color):

let green = g

I know it sounds odd, but that's how the framework has been designed. It may take time to get use to it, but you eventually will.

You may ask, but what if mycolor was created as:

let mycolor = Color.red

In that particular case, you are out of luck :-(

kontiki
  • 37,663
  • 13
  • 111
  • 125
  • 1
    Hmm, so I have the colors of my app change dynamically from a store that returns only Color objects. So even though the solution I tried doesn't work, is there another way to change the tintColor fo the UINavigationBar to the dynamic Color value? – Gavin Jensen Jul 29 '19 at 18:07
  • UINavigationBar is UIKit. You need to wait until SwiftUI _itself_ provides access to the navigation bar. – matt Jul 29 '19 at 18:38