2021年11月12日金曜日

UIColor の Hex String と Color Space

UIColorを、#000000 のような 16進数(Hex String)に変換する方法。ググるといくつか出てくると思う。以下のようなやつ。

func toHexString1(_ color:UIColor) -> String? {
guard let components = color.cgColor.components else { return nil }
return toHexString(components)
}
func toHexString2(_ color:UIColor) -> String? {
var r: CGFloat = 0
var g: CGFloat = 0
var b: CGFloat = 0
var a: CGFloat = 0
guard color.getRed(&r, green: &g, blue: &b, alpha: &a) else { return nil }
return toHexString([r, g, b, a])
}
func toHexString3(_ color:UIColor) -> String? {
let ciColor = CIColor(cgColor: color.cgColor)
return toHexString([ciColor.red, ciColor.green, ciColor.blue, ciColor.alpha])
}
func toHexString(_ components:[CGFloat]) -> String? {
if components.count < 3 { return nil }
let r = Float(components[0])
let g = Float(components[1])
let b = Float(components[2])
let a = components.count >= 4 ? Float(components[3]) : 1.0
if a != 1 {
return String(format: "#%02lX%02lX%02lX%02lX", lroundf(r * 255), lroundf(g * 255), lroundf(b * 255), lroundf(a * 255))
} else {
return String(format: "#%02lX%02lX%02lX", lroundf(r * 255), lroundf(g * 255), lroundf(b * 255))
}
}


toHexString1() は、cgColorのcomponentを使う方法。
toHexString2() は、getRed で取得する方法。
toHexString3() は、CIColor 経由で取得する方法。

コレらをそのまま使うと、変換に失敗する場合もある。

注目すべきは、UIColor が どの Color Spaceが使われているか?

Display P3などは、sRGBよりも大きな範囲が対象になるので、、Extended sRGBに変換すると0.0 から 1.0 に収まらない値、つまり、マイナスや、1.0 より大きい数字になる場合がある。

Display P3 で r, g, b = 1.0, 0.0, 0.0 は、Extended sRGB で、r, g, b = 1.093, -0.227, 0.15 になる。
(使用しているiOSのバージョンによって差異あるかも)


「1. CGColorSpace.displayP3 で作成」と、「2. displayP3Red で作成」は、ともにDisplay P3 の値を指定して作成する方法だが、上記のように構成は変わるみたい。

「2. displayP3Red で作成」の時のように、UIColorをprintで表示した情報と、cgColorの情報は一致しない場合もある。

UIColorPickerViewControllerで取得できる色は、「1. CGColorSpace.displayP3 で作成」の方の構成。
UIColorPickerViewControllerで、Color Spaceを指定できれば良いのになぁ。

また、GrayScaleの場合、componentsの数は2つになることも注意。


安全に Hex Stringを作成するためには、sRGB (or Display P3) に変換してから作成するのが良さそう。

func toHexString4(_ color:UIColor) -> String? {
var cgColor:CGColor = color.cgColor
if let name = cgColor.colorSpace?.name, name != CGColorSpace.sRGB {
cgColor = color.cgColor.converted(to: CGColorSpace(name: CGColorSpace.sRGB)!, intent: .defaultIntent, options: nil)!
}
let ciColor = CIColor(cgColor: cgColor)
return toHexString([ciColor.red, ciColor.green, ciColor.blue, ciColor.alpha])
}



UIColor をアプリに保存する

UserDefaultなどに保存する場合、Hex Stringにする、NSKeyedArchiverでDataにするとがが考えられる。
Display P3の色をNSKeyedArchiverでアーカイブして復元すると、Extended sRGB の構成の色になるみたいだ。
なので、選択した色をできるだけ同じ構成で復元したい場合は、Color Space名、componentsの値の両方を保存するのが良さげかも。


おまけ

Hex StringからUIColor(sRGB)を作成するコード
extension UIColor {
convenience init?(hexString: String) {
let hexString = hexString.trimmingCharacters(in: .whitespacesAndNewlines)
let scanner = Scanner(string: hexString)
scanner.charactersToBeSkipped = CharacterSet(charactersIn: "#")
var color: UInt64 = 0
if scanner.scanHexInt64(&color) {
self.init(hex: color, useOpacity: hexString.count > 7)
} else {
return nil
}
}
convenience init(hex: UInt64, useOpacity: Bool = false) {
let mask = UInt64(0xFF)
let cap = !useOpacity && hex > 0xffffff ? 0xffffff : hex
let r = cap >> (useOpacity ? 24 : 16) & mask
let g = cap >> (useOpacity ? 16 : 8) & mask
let b = cap >> (useOpacity ? 8 : 0) & mask
let a = useOpacity ? cap & mask : 255
self.init(red: CGFloat(r) / 255.0, green: CGFloat(g) / 255.0, blue: CGFloat(b) / 255.0, alpha: CGFloat(a) / 255.0)
}
func toHexString() -> String {
var target = self.cgColor
if let name = cgColor.colorSpace?.name, name != CGColorSpace.sRGB {
let colorSpace = CGColorSpace(name: CGColorSpace.sRGB)!
if let sRGBColor = cgColor.converted(to: colorSpace, intent: .defaultIntent, options: nil) {
target = sRGBColor
}
}
let ciColor = CIColor(cgColor: target)
let r = lroundf(Float(ciColor.red) * 255)
let g = lroundf(Float(ciColor.green) * 255)
let b = lroundf(Float(ciColor.blue) * 255)
let a = lroundf(Float(ciColor.alpha) * 255)
if a != 255 {
return String(format: "#%02lX%02lX%02lX%02lX", r, g, b, a)
} else {
return String(format: "#%02lX%02lX%02lX", r, g, b)
}
}
}

• • •