Why this happens
UIKit’s UIColor only provides a handful of built-in colors (.red, .systemBlue, etc.). Design systems, Figma specs, and backend APIs almost always deliver colors as hexadecimal strings like #FF3B30 or 0xFF3B30. Swift has no built-in initializer that accepts these formats, so every project ends up writing its own conversion code — often copied from Stack Overflow snippets that predate modern Swift.
The root cause is simply API surface area: UIColor expects normalized CGFloat components in the range 0…1, while hex notation packs 8-bit channels into a single integer or string. Bridging that gap once in a reusable extension eliminates repeated bugs (off-by-one shifts, missing alpha, case-sensitive parsing) and keeps color usage consistent across the codebase.
How to fix it
1. Integer-based initializers (recommended for static colors)
This approach uses Swift’s hexadecimal integer literals (0xRRGGBB). It’s evaluated at compile time, has zero runtime parsing overhead, and the compiler catches typos like 0xGG0000.
import UIKit
extension UIColor {
/// Creates a color from 0xRRGGBB with full opacity.
convenience init(rgb: Int) {
self.init(
red: CGFloat((rgb >> 16) & 0xFF) / 255.0,
green: CGFloat((rgb >> 8) & 0xFF) / 255.0,
blue: CGFloat( rgb & 0xFF) / 255.0,
alpha: 1.0
)
}
/// Creates a color from 0xRRGGBB with explicit alpha (0…1).
convenience init(rgb: Int, alpha: CGFloat) {
self.init(
red: CGFloat((rgb >> 16) & 0xFF) / 255.0,
green: CGFloat((rgb >> 8) & 0xFF) / 255.0,
blue: CGFloat( rgb & 0xFF) / 255.0,
alpha: alpha
)
}
/// Creates a color from 0xAARRGGBB (alpha in the high byte).
convenience init(argb: Int) {
self.init(
red: CGFloat((argb >> 16) & 0xFF) / 255.0,
green: CGFloat((argb >> 8) & 0xFF) / 255.0,
blue: CGFloat( argb & 0xFF) / 255.0,
alpha: CGFloat((argb >> 24) & 0xFF) / 255.0
)
}
}
Usage
let systemRed = UIColor(rgb: 0xFF3B30) // opaque
let translucent = UIColor(rgb: 0x007AFF, alpha: 0.3)
let fromARGB = UIColor(argb: 0x80FF3B30) // 50 % alpha red
Why prefer this? No string allocation, no
Scanner, no optionals. The hex literal is validated by the compiler. If you store colors in asset catalogs or design tokens, generate these integer constants at build time.
2. String parsing helper (for dynamic / remote colors)
When colors arrive as #RRGGBB, RRGGBB, #RRGGBBAA, or RRGGBBAA strings from JSON, a CMS, or user input, use a failable initializer that returns nil on malformed input instead of falling back to gray.
extension UIColor {
/// Creates a UIColor from a hex string.
/// Supported formats: "#RRGGBB", "RRGGBB", "#RRGGBBAA", "RRGGBBAA".
/// Returns nil if the string is malformed.
convenience init?(hexString: String) {
var cleaned = hexString.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
if cleaned.hasPrefix("#") { cleaned.removeFirst() }
guard cleaned.count == 6 || cleaned.count == 8 else { return nil }
guard let value = UInt64(cleaned, radix: 16) else { return nil }
let hasAlpha = cleaned.count == 8
let r = CGFloat((value >> (hasAlpha ? 24 : 16)) & 0xFF) / 255.0
let g = CGFloat((value >> (hasAlpha ? 16 : 8)) & 0xFF) / 255.0
let b = CGFloat((value >> (hasAlpha ? 8 : 0)) & 0xFF) / 255.0
let a = hasAlpha ? CGFloat(value & 0xFF) / 255.0 : 1.0
self.init(red: r, green: g, blue: b, alpha: a)
}
}
Usage
if let brandColor = UIColor(hexString: "#007AFF") {
view.backgroundColor = brandColor
}
// With alpha from backend
let dynamic = UIColor(hexString: "007AFF80") // 50 % alpha
Swift 5.9+ tip: Mark the initializer
@_disfavoredOverloadif you also keep the integer initializers, so the compiler prefers the integer version when both match.
3. SwiftUI Color bridge (bonus)
If you mix SwiftUI and UIKit, add a one-liner to use the same hex integers in SwiftUI views:
import SwiftUI
extension Color {
init(rgb: Int) { self.init(UIColor(rgb: rgb)) }
init(argb: Int) { self.init(UIColor(argb: argb)) }
init?(hexString: String) {
guard let ui = UIColor(hexString: hexString) else { return nil }
self.init(ui)
}
}
// SwiftUI view
Rectangle().fill(Color(rgb: 0xFF3B30))
How to prevent it
- Centralize the extension in a shared
UIKitExtensionsmodule orDesignSystempackage so every target uses the exact same conversion logic. - Generate constants from your design-token JSON at build time (e.g., a SwiftGen template) instead of hard-coding hex literals in view code.
- Prefer integer literals for compile-time safety; reserve the string parser for truly dynamic sources.
- Unit-test the parser with a matrix of valid/invalid strings (
#FFF,GGGGGG,#12345,#12345678, etc.) to catch regressions when Swift updatesScanneror integer parsing. - Avoid the old
UIColor(red:green:blue:alpha:)with manual/ 255.0divisions scattered through the codebase — they’re error-prone and hard to grep.
FAQ
Can I use 3-digit hex like #F00?
The parser above intentionally rejects 3-digit shorthand because it’s ambiguous with 6-digit strings missing a leading zero. If you need #RGB support, expand each nibble (F → FF) before the UInt64 conversion.
Why does my color look wrong on wide-gamut displays?
UIColor(rgb:) creates colors in the sRGB color space (the default for init(red:green:blue:alpha:)). For P3 assets, store the color in an asset catalog with the correct color profile and load it via UIColor(named:) instead.
Is there a performance difference between the two approaches?
Yes. The integer initializers compile to a few bit shifts and divisions — essentially zero cost. The string parser allocates, trims, scans, and branches. In a hot path (e.g., cellForRowAt), cache the parsed UIColor or switch to integer constants.
Parts of this guide are adapted from content licensed under CC BY-SA 4.0; this article is shared under the same license.
