Reading time: 3 min

This recipe shows how to use specify SwiftUI Colors with hex values, either hexadecimal numbers or strings.

The end result can look like this:

preview

To make this work, you have to add two extensions to your codebase.

The first one allows you to specify a hex color from an Int, which you can conveniently specify in hexadecimal form. This is also quite a bit quicker to execute than the string version, if perfomance is of any concern to you.

extension Color {
  init(_ hex: UInt, alpha: Double = 1) {
    self.init(
      .sRGB,
      red: Double((hex >> 16) & 0xFF) / 255,
      green: Double((hex >> 8) & 0xFF) / 255,
      blue: Double(hex & 0xFF) / 255,
      opacity: alpha
    )
  }
}

Then, you can use it like this:

let red = Color(0xFF0000)
let green = Color(0x00FF00)
let translucentMagenta = Color(0xFF00FF, alpha: 0.4)

The second extension allows for building a color from a hex string, covering most known formats. It allows for:

  • Specifying color with or without leading #.
  • 2-digit format for shades of gray.
  • 3-digit format for shorthand 6-digit format.
  • 4-digit format for gray with alpha.
  • 6-digit format for RGB.
  • 8-digit format for RGBA.
  • Automatically returns nil for all invalid formats.
extension Color {
  init?(_ hex: String) {
    var str = hex
    if str.hasPrefix("#") {
      str.removeFirst()
    }
    if str.count == 3 {
      str = String(repeating: str[str.startIndex], count: 2) 
        + String(repeating: str[str.index(str.startIndex, offsetBy: 1)], count: 2) 
        + String(repeating: str[str.index(str.startIndex, offsetBy: 2)], count: 2)
    } else if !str.count.isMultiple(of: 2) || str.count > 8 {
      return nil
    }
    guard let color = UInt64(str, radix: 16)
    else {
      return nil
    }
    if str.count == 2 {
      let gray = Double(Int(color) & 0xFF) / 255
      self.init(.sRGB, red: gray, green: gray, blue: gray, opacity: 1)
    } else if str.count == 4 {
      let gray = Double(Int(color >> 8) & 0x00FF) / 255
      let alpha = Double(Int(color) & 0x00FF) / 255
      self.init(.sRGB, red: gray, green: gray, blue: gray, opacity: alpha)
    } else if str.count == 6 {
      let red = Double(Int(color >> 16) & 0x0000FF) / 255
      let green = Double(Int(color >> 8) & 0x0000FF) / 255
      let blue = Double(Int(color) & 0x0000FF) / 255
      self.init(.sRGB, red: red, green: green, blue: blue, opacity: 1)
    } else if str.count == 8 {
      let red = Double(Int(color >> 24) & 0x000000FF) / 255
      let green = Double(Int(color >> 16) & 0x000000FF) / 255
      let blue = Double(Int(color >> 8) & 0x000000FF) / 255
      let alpha = Double(Int(color) & 0x000000FF) / 255
      self.init(.sRGB, red: red, green: green, blue: blue, opacity: alpha)
    } else {
      return nil
    }
  }
}

You can find examples online of similar code that use Scanner with scanHexInt64 to convert the hex string to a number, but that function can produce false positives. E.g, if you feed it with "fail", it still returns a valid number equivalent to fa, while completely ignoring the incorrect il at the end.

And here are some sample colors, demonstrating all the supported formats:

let gray1 = Color("4f")
let gray2 = Color("#68")
let gray3 = Color("7813")
let red = Color("f00")
let translucentGreen = Color("#00FF0066")
let blue = Color("0000FF")
let invalid = Color("0000F")

Next Post Previous Post