Fix: Using Hex Color Values in Swift UIKit

Fix: Using Hex Color Values in Swift UIKit

Written by

in

Quick answer: Add a UIColor extension with convenience initializers that accept 0xRRGGBB integers or a helper that parses #RRGGBB strings. Both approaches avoid string parsing at runtime and work on iOS 15+. Use the integer version for compile-time safety and the string version when colors come from JSON or user input.

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 @_disfavoredOverload if 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 UIKitExtensions module or DesignSystem package 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 updates Scanner or integer parsing.
  • Avoid the old UIColor(red:green:blue:alpha:) with manual / 255.0 divisions 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.


Sources & credits

Parts of this guide are adapted from content licensed under CC BY-SA 4.0; this article is shared under the same license.