Create your own color picker in Swift

John Arnaoutakis
6 min readMar 3, 2022

In this post I will share with you how to create the basic components of a color picker. Let’s start by creating a spectrum view but first let’s talk about what is a spectrum.

A spectrum ranges through all the colors visible to our eyes.

We need to express all of the colors in a nice view that ranges through every color of the spectrum. Well if that sounds to you like a gradient that has many colors, you are correct! Let’s create the SpectrumView.

class SpectrumView: UIView {

let gradientLayer = CAGradientLayer()

override func draw(_ rect: CGRect) {
super.draw(rect)

setUpGradientLayerIfNeeded()
}

private func setUpGradientLayerIfNeeded() {
guard gradientLayer.superlayer == nil else { return }
gradientLayer.colors = [
UIColor(red: 1, green: 0, blue: 0, alpha: 1).cgColor,
UIColor(red: 1, green: 1, blue: 0, alpha: 1).cgColor,
UIColor(red: 0, green: 1, blue: 0, alpha: 1).cgColor,
UIColor(red: 0, green: 1, blue: 1, alpha: 1).cgColor,
UIColor(red: 0, green: 0, blue: 1, alpha: 1).cgColor,
UIColor(red: 1, green: 0, blue: 1, alpha: 1).cgColor,
UIColor(red: 1, green: 0, blue: 0, alpha: 1).cgColor
]
gradientLayer.startPoint = CGPoint(x: 0, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 1, y: 0.5)
gradientLayer.frame = bounds
gradientLayer.name = gradientName
layer.addSublayer(gradientLayer)
}
}

A gradient is drawn by ranging through one color to another.

In the above code first we check if our gradient layer exists, we only need to create it once. We give to the gradient layer all the colors by starting to increase the values and decrease them to return to the first one. The first is red, then it turns to yellow then green and so on. While the gradient draws it automatically generates the colors between those values.

After specifying the colors we define a start point at the very left and middle of the view (vertically). The end point is still at the middle of the view but at the very right point of the view. This makes the gradient go from left to right and have this nice result below.

The next step is to get a color from this spectrum. In order to do that we are going to create a new method in a layer extension. In order to help you understand this code I left some comments on every relevant line. In short we are using the given point to extract the color of a pixel and convert it to a UIColor.

extension CALayer {    func colorOfPoint(point: CGPoint) -> UIColor? {
/// Our pixel data
var pixel: [UInt8] = [0, 0, 0, 0]
/// The device's color space
let colorSpace = CGColorSpaceCreateDeviceRGB()
/// Get the pixel data already multiplied by the alpha value
let bitmapInfo = CGBitmapInfo(
rawValue: CGImageAlphaInfo.premultipliedLast.rawValue
)

/// try to get a context of 1x1 pixel by getting 8 bits
/// per component in the given color space
guard let context = CGContext(
data: &pixel,
width: 1,
height: 1,
bitsPerComponent: 8,
bytesPerRow: 4,
space: colorSpace,
bitmapInfo: bitmapInfo.rawValue
) else { return nil }
context.translateBy(x: -point.x, y: -point.y)

render(in: context)
/// Get every value from the array
let red = CGFloat(pixel[0]) / 255.0
let green = CGFloat(pixel[1]) / 255.0
let blue = CGFloat(pixel[2]) / 255.0
let alpha = CGFloat(pixel[3]) / 255.0

/// Create the color from the values
return UIColor(red: red, green: green, blue: blue, alpha: alpha)
}
}

Now that we have this handy extension we can use a UITapGestureRecognizer to get a point and pass it to this method in order to extract the tapped color.

In the spectrum view class add a gesture recognizer and use the layer reference to get a color of point. We can create a protocol as well to delegate this color. The final SpectrumView class should look like this:

protocol SpectrumViewDelegate: AnyObject {
func spectrumView(
_ view: SpectrumView,
didSelect color: UIColor
)
}
class SpectrumView: UIView {

let gradientLayer = CAGradientLayer()
weak var delegate: SpectrumViewDelegate? override func draw(_ rect: CGRect) {
super.draw(rect)

setUpGradientLayerIfNeeded()
}

private func setUpGradientLayerIfNeeded() {
guard gradientLayer.superlayer == nil else { return }
gradientLayer.colors = [
UIColor(red: 1, green: 0, blue: 0, alpha: 1).cgColor,
UIColor(red: 1, green: 1, blue: 0, alpha: 1).cgColor,
UIColor(red: 0, green: 1, blue: 0, alpha: 1).cgColor,
UIColor(red: 0, green: 1, blue: 1, alpha: 1).cgColor,
UIColor(red: 0, green: 0, blue: 1, alpha: 1).cgColor,
UIColor(red: 1, green: 0, blue: 1, alpha: 1).cgColor,
UIColor(red: 1, green: 0, blue: 0, alpha: 1).cgColor
]
gradientLayer.startPoint = CGPoint(x: 0, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 1, y: 0.5)
gradientLayer.frame = bounds
gradientLayer.name = gradientName
layer.addSublayer(gradientLayer)
addGestureRecognizer(
UITapGestureRecognizer(
target: self,
action: #selector(onTap)
)
)
}
@objc func onTap(_ gestureRecognizer: UITapGestureRecognizer) {
guard let color = gradientLayer.colorOfPoint(
point: gestureRecognizer.location(in: self)
) else { return }
delegate?.spectrumView(self, didSelect: color)
}
}

That’s it! We now have a view that displays all the colors and with a tap we can get a color from it. Of course you can enrich the functionality and use a UIPanGestureRecognizer to make the user pan and select every color as he/she pans.

It would be much nicer to let our users also select the saturation and brightness of the color. This way they can customize them more. Each color is represented by a hue. You can tweak with the brightenss and the saturation of a hue. Let’s do that!

protocol SaturationBrightnessViewDelegate: AnyObject {
func saturationBrightnessView(
_ view: SaturationBrightnessView,
didSelect color: UIColor
)
}
class SaturationBrightnessView: UIView {

private var color: UIColor = .red

weak var delegate: SaturationBrightnessViewDelegate?

override func draw(_ rect: CGRect) {
super.draw(rect)
guard let context = UIGraphicsGetCurrentContext()
else { return }
var hue: CGFloat = 0
var saturation: CGFloat = 0
var brightness: CGFloat = 0
var alpha: CGFloat = 0
color.getHue(
&hue,
saturation: &saturation,
brightness: &brightness,
alpha: &alpha
)
let colorSpace = CGColorSpaceCreateDeviceRGB()
guard let colorGradient = CGGradient(
colorsSpace: colorSpace,
colors: [
UIColor(hue: hue, saturation: 1, brightness: 1, alpha: 1).cgColor,
UIColor(hue: hue, saturation: 0, brightness: 1, alpha: 1).cgColor
] as CFArray,
locations: nil
), let backgroundGradient = CGGradient(
colorsSpace: colorSpace, colors: [
UIColor(hue: hue, saturation: 0, brightness: 0, alpha: 0).cgColor,
UIColor(hue: hue, saturation: 0, brightness: 0, alpha: 1).cgColor
] as CFArray,
locations: nil
) else { return }

context.saveGState()
context.addRect(rect)
context.clip()
context.drawLinearGradient(
colorGradient,
start: CGPoint(x: rect.maxX, y: rect.maxY), end: CGPoint(x: 0, y: rect.maxY),
options: CGGradientDrawingOptions()
)
context.drawLinearGradient(
backgroundGradient,
start: CGPoint(x: 0, y: 0), end: CGPoint(x: 0, y: rect.maxY),
options: CGGradientDrawingOptions()
)
context.restoreGState()

gestureRecognizers?.removeAll()
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onTap(_:))))
}

func updateColor(with color: UIColor) {
self.color = color
setNeedsDisplay()
}

@objc func onTap(_ gestureRecognizer: UITapGestureRecognizer) {
let location = gestureRecognizer.location(in: self)
guard let color = layer.colorOfPoint(point: location) else { return }
delegate?.saturationBrightnessView(self, didSelect: color)
}
}

So in the above code, instead of creating a signle gradient ranging through colors we are using Core Graphics to draw the current layer of the view. We start with what I call the color layer which creates a gradient from color with saturation at 100% and brightness at 100% and ranges to the same color but with 0% saturation. Then we create background gradient which has 0% saturation and 0% brightness but the alpha value goes from 100% to 0%. This way the two layers blend together to create the new view. We draw both layers and the result looks great. I added some protocols and functionality in order to get the color with a tap as we did before.

By combining the delegates you get this nice functionality in conjunction with a preview view.

Here is a link with the example playground for you to dig deeper, add some pan gesture recognizers and enhance it more!

--

--