Skip to content

Instantly share code, notes, and snippets.

@sam-w
Last active February 23, 2022 20:44
Show Gist options
  • Save sam-w/1ec82fe3809b3da1392b77241baeabb8 to your computer and use it in GitHub Desktop.
Save sam-w/1ec82fe3809b3da1392b77241baeabb8 to your computer and use it in GitHub Desktop.
import SwiftUI
import UIKit
extension Color {
var uiColor: UIColor {
if #available(iOS 14, *) {
// iOS 14 introduces an API to convert SwiftUI.Color to UIKit.UIColor
// but it does not produce a color which reacts to changes in color scheme
// (light mode/dark mode). To make that work we need to extract the color
// name and go back to the asset catalog.
if let color = try! UIColor.namedColor(from: stringRepresentation) {
return color
}
if let opacityColor = try! OpacityColor(color: self),
let color = try! UIColor.namedColor(from: opacityColor.stringRepresentation)
{
return color.multiplyingAlphaComponent(by: opacityColor.opacityModifier)
}
return UIColor(self)
} else {
return try! convertToUIColor()
}
}
}
private enum ColorConversionError: Error {
case couldNotParseInternalType(source: String)
case couldNotParseSystemColor(source: String)
case couldNotParseHex(source: String)
case couldNotParseUIColorComponents(source: String)
case couldNotParseCGColorRef(source: String)
case couldNotParseP3Components(source: String)
case couldNotParseNamedColor(source: String)
case invalidInternalType(String)
case invalidColorSpace(String)
}
private extension Color {
var stringRepresentation: String {
description.trimmingCharacters(in: .whitespacesAndNewlines)
}
var internalType: String {
String(describing: 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 color.convertToUIColor()
}
return try UIColor.from(swiftUIDescription: stringRepresentation, internalType: internalType)
}
}
private 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 dumpString = ""
dump(color, to: &dumpString)
dumpString = dumpString.replacingOccurrences(
of: #"^(?:.*\n){\#(4 * opacityLayerCount)}.*?base: "#,
with: "",
options: .regularExpression
)
let opacityModifier = dumpString
.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: dumpString,
options: [],
range: NSRange(dumpString.startIndex ..< dumpString.endIndex, in: dumpString)
)
guard let match = matches.first, matches.count == 1, match.numberOfRanges == 2 else {
throw ColorConversionError.couldNotParseInternalType(source: dumpString)
}
self.init(
stringRepresentation: String(dumpString.prefix { !$0.isNewline }),
internalType: String(dumpString[Range(match.range(at: 1), in: dumpString)!]),
opacityModifier: opacityModifier
)
}
func convertToUIColor() throws -> UIColor {
return try UIColor.from(
swiftUIDescription: stringRepresentation,
internalType: internalType
)
.multiplyingAlphaComponent(
by: opacityModifier
)
}
}
private extension UIColor {
static func from(swiftUIDescription description: String, internalType: String) throws -> UIColor {
switch internalType {
case "SystemColorType":
guard let color = systemColor(from: description) else {
throw ColorConversionError.couldNotParseSystemColor(source: description)
}
return color
case "_Resolved":
guard let color = try resolvedColor(from: description) ?? systemColor(from: description) else {
throw ColorConversionError.couldNotParseHex(source: description)
}
return color
case "UIColor":
guard let color = try uiColor(from: description) else {
throw ColorConversionError.couldNotParseUIColorComponents(source: description)
}
return color
case "CGColorRef":
guard let color = try cgColorRef(from: description) else {
throw ColorConversionError.couldNotParseCGColorRef(source: description)
}
return color
case "DisplayP3":
guard let color = try p3Color(from: description) else {
throw ColorConversionError.couldNotParseP3Components(source: description)
}
return color
case "NamedColor":
guard let color = try namedColor(from: description) else {
throw ColorConversionError.couldNotParseNamedColor(source: description)
}
return color
default:
throw ColorConversionError.invalidInternalType(internalType)
}
}
static func systemColor(from description: String) -> UIColor? {
switch description {
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
}
}
static func resolvedColor(from description: String) throws -> UIColor? {
guard description.range(of: "^#[0-9A-F]{8}$", options: .regularExpression) != nil else {
return nil
}
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.couldNotParseHex(source: description)
}
return UIColor(cgColor: cgColor)
}
static func uiColor(from description: String) throws -> 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 {
return nil
}
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)
}
static func cgColorRef(from description: String) throws -> UIColor? {
// TODO: Parse colorRef string, e.g.:
// "<CGColor 0x600001d98000> [<CGColorSpace 0x600001d984e0> (kCGColorSpaceICCBased; kCGColorSpaceModelRGB; sRGB IEC61966-2.1)] ( 1 0 0 1 )"
return nil
}
static func p3Color(from description: String) throws -> UIColor? {
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 {
return nil
}
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.couldNotParseP3Components(source: description)
}
let (r, g, b, a) = (components[0], components[1], components[2], components[3])
return UIColor(displayP3Red: r, green: g, blue: b, alpha: a)
}
static func namedColor(from description: String) throws -> UIColor? {
guard description.range(of: #"^NamedColor\(name: "(.*)", bundle: .*\)$"#, options: .regularExpression) != nil else {
return nil
}
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.couldNotParseNamedColor(source: 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)!
}
}
private extension UIColor {
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.invalidColorSpace(colorSpace)
}
}
func multiplyingAlphaComponent(by multiplier: CGFloat?) -> UIColor {
var a: CGFloat = 0
getWhite(nil, alpha: &a)
return withAlphaComponent(a * (multiplier ?? 1))
}
}
// MARK: Helper extensions
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)
}
}
extension Collection {
/**
Splits this collection into a collection of chunks, each with a maximum size of `size`.
If the collection does not divide exactly, the final chunk may contain fewer elements.
*/
func chunks(of size: Int) -> [[Element]] {
stride(from: 0, to: count, by: size).map {
let start = index(startIndex, offsetBy: $0)
let end = index(start, offsetBy: size, limitedBy: endIndex) ?? endIndex
return Array(self[start ..< end])
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment