From Hex String to SwiftUI Color—and Back Again
Practical Strategies, Detailed Explanations, and Production-Ready Code for Every Project
Hex color strings are not only convenient to use in code, but they also represent one of the best ways to store, configure, and transmit colors in real world applications.
Hex color values are found in design tools, APIs, config files, databases, and even user settings. They're short, human readable, cross-platform, and everybody, designers and developers, understands them. Because they are storage friendly, you can drive your application's color scheme easily from JSON, remote config, or your users preferences without any special parsing or encoding.
The only catch? SwiftUI's Color struct does not support hex strings natively.
This article will walk through your core extension, explaining what is going on, how it works, and why it works—bitwise math, platform quirks, and what can go wrong—so that you can use it in any project with confidence and even adapt it to your own code.
Part 1: Converting a Hex String to SwiftUI Color
(Let's analyze your extension line by line.)
extension Color {
init?(hex: String) {
var hexSanitized = hex.trimmingCharacters(in: .whitespacesAndNewlines)
hexSanitized = hexSanitized.replacingOccurrences(of: "#", with: "")
var rgb: UInt64 = 0
var r: CGFloat = 0.0
var g: CGFloat = 0.0
var b: CGFloat = 0.0
var a: CGFloat = 1.0
let length = hexSanitized.count
guard Scanner(string: hexSanitized).scanHexInt64(&rgb) else { return nil }
if length == 6 {
r = CGFloat((rgb & 0xFF0000) >> 16) / 255.0
g = CGFloat((rgb & 0x00FF00) >> 8) / 255.0
b = CGFloat(rgb & 0x0000FF) / 255.0
} else if length == 8 {
r = CGFloat((rgb & 0xFF000000) >> 24) / 255.0
g = CGFloat((rgb & 0x00FF0000) >> 16) / 255.0
b = CGFloat((rgb & 0x0000FF00) >> 8) / 255.0
a = CGFloat(rgb & 0x000000FF) / 255.0
} else {
return nil
}
self.init(red: r, green: g, blue: b, opacity: a)
}
}
1. Sanitizing the Input
var hexSanitized = hex.trimmingCharacters(in: .whitespacesAndNewlines)
hexSanitized = hexSanitized.replacingOccurrences(of: "#", with: "")
Why:
Design handoff or APIs might give you
#1ABC9C
,1abc9c
, etc.This strips whitespace and the optional
#
prefix, leaving only the hex digits.Clean inputs are critical for safe parsing.
2. Setting Up Variables
var rgb: UInt64 = 0
var r: CGFloat = 0.0
var g: CGFloat = 0.0
var b: CGFloat = 0.0
var a: CGFloat = 1.0
Why:
rgb will hold the parsed hex number.
Default color channels are zero, alpha is set to 1.0 (fully opaque) unless specified.
3. Getting Length and Validating
let length = hexSanitized.count
guard Scanner(string: hexSanitized).scanHexInt64(&rgb) else { return nil }
Why:
Only hex strings of length 6 (RRGGBB) or 8 (RRGGBBAA) are valid.
scanHexInt64 parses the hex string as an integer. Any invalid character, you get nil—safer than assuming inputs are perfect.
4. Bitwise Extraction: Unpacking Channels
6 Digits (No Alpha):
if length == 6 {
r = CGFloat((rgb & 0xFF0000) >> 16) / 255.0
g = CGFloat((rgb & 0x00FF00) >> 8) / 255.0
b = CGFloat(rgb & 0x0000FF) / 255.0
}
Why:
Each color is two hex digits (one byte), so we mask and shift.
Divide by 255 to convert from 0–255 to 0–1 (which is what Color expects).
Example:
For #336699
(or 336699
):
r
= 0x33 = 51/255 ≈ 0.2g
= 0x66 = 102/255 ≈ 0.4b
= 0x99 = 153/255 ≈ 0.6
8 Digits (With Alpha):
else if length == 8 {
r = CGFloat((rgb & 0xFF000000) >> 24) / 255.0
g = CGFloat((rgb & 0x00FF0000) >> 16) / 255.0
b = CGFloat((rgb & 0x0000FF00) >> 8) / 255.0
a = CGFloat(rgb & 0x000000FF) / 255.0
}
Why:
Same logic, but also unpacks alpha from the lowest two hex digits.
Example:
For FF3366CC:
r
: 0xFF = 255/255 = 1.0g
: 0x33 = 51/255 ≈ 0.2b
: 0x66 = 102/255 ≈ 0.4a
: 0xCC = 204/255 ≈ 0.8
5. Handling Invalid Lengths
else {
return nil
}
Anything not 6 or 8 hex digits is rejected.
Returning nil avoids crashes and lets the caller handle errors.
6. The Final Step
self.init(red: r, green: g, blue: b, opacity: a)
Pass the normalized values to SwiftUI
Color
.
Part 2: Converting SwiftUI Color
Back to Hex
Your toHex() extension enables you to output the color as a hex string, for sharing, theming, or saving in config files.
func toHex() -> String? {
let uic = UIColor(self)
guard let components = uic.cgColor.components, components.count >= 3 else {
return nil
}
let r = Float(components[0])
let g = Float(components[1])
let b = Float(components[2])
var a = Float(1.0)
if components.count >= 4 {
a = Float(components[3])
}
if a != Float(1.0) {
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))
}
}
1. Bridging to UIKit
let uic = UIColor(self)
Converts SwiftUI Color to UIKit UIColor for easier channel extraction.
On macOS, you’d need to adapt this (e.g. with NSColor).
2. Extracting RGBA Components
guard let components = uic.cgColor.components, components.count >= 3 else {
return nil
}
let r = Float(components[0])
let g = Float(components[1])
let b = Float(components[2])
var a = Float(1.0)
if components.count >= 4 {
a = Float(components[3])
}
Grabs the first three floats as R, G, B (all between 0 and 1).
If there’s a 4th, that’s alpha; if not, defaults to 1.0.
3. Formatting the Hex String
if a != Float(1.0) {
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))
}
Multiplies each channel by 255 and rounds to an integer in 0…255.
%02lX outputs each as a 2-digit uppercase hex (padded with 0 if needed).
Only includes alpha if not 1.0 (matches common design/API expectations).
Example:
Red: Color(red: 1, green: 0.5, blue: 0, opacity: 0.5)
Hex output:
r: 1.0 * 255 = 255 → FF
g: 0.5 * 255 = 128 → 80
b: 0.0 * 255 = 0 → 00
a: 0.5 * 255 = 128 → 80
Returns
"
FF800080"
4. What Can Go Wrong?
Some colors aren’t decomposed as simple RGB(A) (rare, but grayscale/pattern colors exist).
On macOS, or if using complex color spaces, you’ll need platform-specific extraction.
Summary Table
All channel values are normalized to 0–1 before passing to SwiftUI.
Conclusion:
Hex color parsing is just bitwise math and normalization:
Sanitize your string.
Parse it as an integer.
Mask and shift to extract the channels.
Divide by 255 to convert to SwiftUI's 0 - 1 format.
The reverse conversion (Color to hex), uses the same normalization but in reverse:
Get the 0 - 1 value from UIColor.
Multiply by 255, round, and convert to two-digit hex.
Write out 6 digits (RGB) or 8 digits (RGBA), as required.
With the principles outlined above, you can build color extensions that work on iOS, are in line with design specs, and work seamlessly with APIs or theme engines.
Copy, paste, adapt and most importantly understand! That's how you stay ahead as an honest to goodness iOS developer!