Simple swift color picker popover (iOS)

52.4k Views Asked by At

Is there is a simple way to implement a color picker popover in swift? Are there any built-in libraries or UI elements that I could leverage for this purpose? I saw some color pickers written in objective-c, but they were several years old and I was wondering if there was something more recent.

8

There are 8 best solutions below

1
On BEST ANSWER

With iOS 14 Apple has now implemented a standard UIColorPickerViewController and associated UIColorWell that is a color swatch that automatically brings up the UIColorPicker to choose a color.

You can test the ColorPicker by creating a Swift App project with Xcode 12 or later, targeting iOS 14+ and then try this simple code:

import SwiftUI

struct ContentView: View {
    @State private var bgColor = Color.white

    var body: some View {
        VStack {
            ColorPicker("Set the background color",
                        selection: $bgColor,
                        supportsOpacity: true)
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(bgColor)
    }
}

Change supportsOpacity to false to get rid of the opacity slider and only allow fully opaque colors.

ColorPicker showing two different modes of selection:

iOS 14 ColorPicker

ColorPicker without alpha:

iOS ColorPicker no alpha

0
On

Swift 3.0 version of @joel-teply's answer:

internal protocol HSBColorPickerDelegate : NSObjectProtocol {
    func HSBColorColorPickerTouched(sender:HSBColorPicker, color:UIColor, point:CGPoint, state:UIGestureRecognizerState)
}

@IBDesignable
class HSBColorPicker : UIView {

    weak internal var delegate: HSBColorPickerDelegate?
    let saturationExponentTop:Float = 2.0
    let saturationExponentBottom:Float = 1.3

    @IBInspectable var elementSize: CGFloat = 1.0 {
        didSet {
            setNeedsDisplay()
        }
    }


    private func initialize() {

        self.clipsToBounds = true
        let touchGesture = UILongPressGestureRecognizer(target: self, action: #selector(self.touchedColor(gestureRecognizer:)))
        touchGesture.minimumPressDuration = 0
        touchGesture.allowableMovement = CGFloat.greatestFiniteMagnitude
        self.addGestureRecognizer(touchGesture)
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        initialize()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        initialize()
    }

    override func draw(_ rect: CGRect) {
        let context = UIGraphicsGetCurrentContext()

        for y in stride(from: (0 as CGFloat), to: rect.height, by: elementSize) {

            var saturation = y < rect.height / 2.0 ? CGFloat(2 * y) / rect.height : 2.0 * CGFloat(rect.height - y) / rect.height
            saturation = CGFloat(powf(Float(saturation), y < rect.height / 2.0 ? saturationExponentTop : saturationExponentBottom))
            let brightness = y < rect.height / 2.0 ? CGFloat(1.0) : 2.0 * CGFloat(rect.height - y) / rect.height

            for x in stride(from: (0 as CGFloat), to: rect.width, by: elementSize) {
                let hue = x / rect.width
                let color = UIColor(hue: hue, saturation: saturation, brightness: brightness, alpha: 1.0)
                context!.setFillColor(color.cgColor)
                context!.fill(CGRect(x:x, y:y, width:elementSize,height:elementSize))
            }
        }
    }

    func getColorAtPoint(point:CGPoint) -> UIColor {
        let roundedPoint = CGPoint(x:elementSize * CGFloat(Int(point.x / elementSize)),
                                   y:elementSize * CGFloat(Int(point.y / elementSize)))
        var saturation = roundedPoint.y < self.bounds.height / 2.0 ? CGFloat(2 * roundedPoint.y) / self.bounds.height
            : 2.0 * CGFloat(self.bounds.height - roundedPoint.y) / self.bounds.height
        saturation = CGFloat(powf(Float(saturation), roundedPoint.y < self.bounds.height / 2.0 ? saturationExponentTop : saturationExponentBottom))
        let brightness = roundedPoint.y < self.bounds.height / 2.0 ? CGFloat(1.0) : 2.0 * CGFloat(self.bounds.height - roundedPoint.y) / self.bounds.height
        let hue = roundedPoint.x / self.bounds.width
        return UIColor(hue: hue, saturation: saturation, brightness: brightness, alpha: 1.0)
    }

    func getPointForColor(color:UIColor) -> CGPoint {
        var hue:CGFloat=0;
        var saturation:CGFloat=0;
        var brightness:CGFloat=0;
        color.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: nil);

        var yPos:CGFloat = 0
        let halfHeight = (self.bounds.height / 2)

        if (brightness >= 0.99) {
            let percentageY = powf(Float(saturation), 1.0 / saturationExponentTop)
            yPos = CGFloat(percentageY) * halfHeight
        } else {
            //use brightness to get Y
            yPos = halfHeight + halfHeight * (1.0 - brightness)
        }

        let xPos = hue * self.bounds.width

        return CGPoint(x: xPos, y: yPos)
    }

    func touchedColor(gestureRecognizer: UILongPressGestureRecognizer){
        let point = gestureRecognizer.location(in: self)
        let color = getColorAtPoint(point: point)

        self.delegate?.HSBColorColorPickerTouched(sender: self, color: color, point: point, state:gestureRecognizer.state)
    }
}
0
On

ColorPickerViewImage

Based on Christian1313 answer, I added darker colors:

@IBDesignable final public class SwiftColorView: UIView {

weak var colorSelectedDelegate: ColorDelegate?

@IBInspectable public var numColorsX:Int =  10 {
    didSet {
        setNeedsDisplay()
    }
}

@IBInspectable public var numColorsY:Int = 18 {
    didSet {
        setNeedsDisplay()
    }
}

@IBInspectable public var coloredBorderWidth:Int = 10 {
    didSet {
        setNeedsDisplay()
    }
}

@IBInspectable public var showGridLines:Bool = false {
    didSet {
        setNeedsDisplay()
    }
}

weak var delegate: SwiftColorPickerDataSource?

public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    guard let touch = touches.first else { return }
    let location = touch.location(in: self)

    colorSelectedDelegate?.setStroke(color: colorAtPoint(point: location))
}

public override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
    guard let touch = touches.first else { return }
    let location = touch.location(in: self)

    colorSelectedDelegate?.setStroke(color: colorAtPoint(point: location))
}

public override func draw(_ rect: CGRect) {
    super.draw(rect)
    let lineColor = UIColor.gray
    let pS = patternSize()
    let w = pS.w
    let h = pS.h

    for y in 0..<numColorsY
    {
        for x in 0..<numColorsX
        {
            let path = UIBezierPath()
            let start = CGPoint(x: CGFloat(x)*w+CGFloat(coloredBorderWidth), y: CGFloat(y)*h+CGFloat(coloredBorderWidth))
            path.move(to: start);
            path.addLine(to: CGPoint(x: start.x+w, y: start.y))
            path.addLine(to: CGPoint(x: start.x+w, y: start.y+h))
            path.addLine(to: CGPoint(x: start.x, y: start.y+h))
            path.addLine(to: start)
            path.lineWidth = 0.25
            colorForRectAt(x: x,y:y).setFill();

            if (showGridLines)
            {
                lineColor.setStroke()
            }
            else
            {
                colorForRectAt(x: x, y: y).setStroke();
            }
            path.fill();
            path.stroke();
        }
    }
}

private func colorForRectAt(x: Int, y: Int) -> UIColor
{

    if let ds = delegate {
        return ds.colorForPalletIndex(x: x, y: y, numXStripes: numColorsX, numYStripes: numColorsY)
    } else {

        var hue:CGFloat = CGFloat(x) / CGFloat(numColorsX)
        var fillColor = UIColor.white
        if (y==0)
        {
            if (x==(numColorsX-1))
            {
                hue = 1.0;
            }
            fillColor = UIColor(white: hue, alpha: 1.0);
        }
        else
        {
            if y < numColorsY / 2 {
                //dark
                let length = numColorsY / 2
                let brightness: CGFloat = CGFloat(y) / CGFloat(length)
                fillColor = UIColor(hue: hue, saturation: 1.0, brightness: brightness, alpha: 1.0)
            } else if y == numColorsY / 2 {
                // normal
                fillColor = UIColor(hue: hue, saturation: 1.0, brightness: 1.0, alpha: 1.0)
            } else {
                // light
                let length = numColorsY / 2 - 1
                let offset = y - length - 1
                let sat:CGFloat = CGFloat(1.0) - CGFloat(offset) / CGFloat(length + 1)
                print("sat", sat)
                fillColor = UIColor(hue: hue, saturation: sat, brightness: 1.0, alpha: 1.0)
            }
        }
        return fillColor
    }
}

func colorAtPoint(point: CGPoint) -> UIColor
{
    let pS = patternSize()
    let w = pS.w
    let h = pS.h
    let x = (point.x-CGFloat(coloredBorderWidth))/w
    let y = (point.y-CGFloat(coloredBorderWidth))/h
    return colorForRectAt(x: Int(x), y:Int(y))
}

private func patternSize() -> (w: CGFloat, h:CGFloat)
{
    let width = self.bounds.width-CGFloat(2*coloredBorderWidth)
    let height = self.bounds.height-CGFloat(2*coloredBorderWidth)

    let w = width/CGFloat(numColorsX)
    let h = height/CGFloat(numColorsY)
    return (w,h)
}

public override func prepareForInterfaceBuilder()
{
    print("Compiled and run for IB")
}

}

8
On

Here's one I made which is as simple as it gets. It's just a lightweight UIView that allows you to specify the element size in case you want blocked regions (elementSize > 1). It draws itself in interface builder so you can set element size and see the consequences. Just set one of your views in interface builder to this class and then set yourself as a delegate. It will tell you when someone either taps or drags on it and the uicolor at that location. It will draw itself to its own bounds and there's no need for anything other than this class, no image required.

Element size=1 (Default) element size=1

Element size=10
element size=10

internal protocol HSBColorPickerDelegate : NSObjectProtocol {
    func HSBColorColorPickerTouched(sender:HSBColorPicker, color:UIColor, point:CGPoint, state:UIGestureRecognizerState)
}

@IBDesignable
class HSBColorPicker : UIView {

    weak internal var delegate: HSBColorPickerDelegate?
    let saturationExponentTop:Float = 2.0
    let saturationExponentBottom:Float = 1.3

    @IBInspectable var elementSize: CGFloat = 1.0 {
        didSet {
            setNeedsDisplay()
        }
    }

    private func initialize() {
        self.clipsToBounds = true
        let touchGesture = UILongPressGestureRecognizer(target: self, action: #selector(self.touchedColor(gestureRecognizer:)))
        touchGesture.minimumPressDuration = 0
        touchGesture.allowableMovement = CGFloat.greatestFiniteMagnitude
        self.addGestureRecognizer(touchGesture)
    }

   override init(frame: CGRect) {
        super.init(frame: frame)
        initialize()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        initialize()
    }

    override func draw(_ rect: CGRect) {
        let context = UIGraphicsGetCurrentContext()
        for y : CGFloat in stride(from: 0.0 ,to: rect.height, by: elementSize) {
            var saturation = y < rect.height / 2.0 ? CGFloat(2 * y) / rect.height : 2.0 * CGFloat(rect.height - y) / rect.height
            saturation = CGFloat(powf(Float(saturation), y < rect.height / 2.0 ? saturationExponentTop : saturationExponentBottom))
            let brightness = y < rect.height / 2.0 ? CGFloat(1.0) : 2.0 * CGFloat(rect.height - y) / rect.height
            for x : CGFloat in stride(from: 0.0 ,to: rect.width, by: elementSize) {
                let hue = x / rect.width
                let color = UIColor(hue: hue, saturation: saturation, brightness: brightness, alpha: 1.0)
                context!.setFillColor(color.cgColor)
                context!.fill(CGRect(x:x, y:y, width:elementSize,height:elementSize))
            }
        }
    }

    func getColorAtPoint(point:CGPoint) -> UIColor {
        let roundedPoint = CGPoint(x:elementSize * CGFloat(Int(point.x / elementSize)),
                               y:elementSize * CGFloat(Int(point.y / elementSize)))
        var saturation = roundedPoint.y < self.bounds.height / 2.0 ? CGFloat(2 * roundedPoint.y) / self.bounds.height
        : 2.0 * CGFloat(self.bounds.height - roundedPoint.y) / self.bounds.height
        saturation = CGFloat(powf(Float(saturation), roundedPoint.y < self.bounds.height / 2.0 ? saturationExponentTop : saturationExponentBottom))
        let brightness = roundedPoint.y < self.bounds.height / 2.0 ? CGFloat(1.0) : 2.0 * CGFloat(self.bounds.height - roundedPoint.y) / self.bounds.height
        let hue = roundedPoint.x / self.bounds.width
        return UIColor(hue: hue, saturation: saturation, brightness: brightness, alpha: 1.0)
    }

    func getPointForColor(color:UIColor) -> CGPoint {
        var hue: CGFloat = 0.0
        var saturation: CGFloat = 0.0
        var brightness: CGFloat = 0.0
        color.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: nil);

        var yPos:CGFloat = 0
        let halfHeight = (self.bounds.height / 2)
        if (brightness >= 0.99) {
            let percentageY = powf(Float(saturation), 1.0 / saturationExponentTop)
            yPos = CGFloat(percentageY) * halfHeight
        } else {
            //use brightness to get Y
            yPos = halfHeight + halfHeight * (1.0 - brightness)
        }
        let xPos = hue * self.bounds.width
        return CGPoint(x: xPos, y: yPos)
    }

    @objc func touchedColor(gestureRecognizer: UILongPressGestureRecognizer) {
        if (gestureRecognizer.state == UIGestureRecognizerState.began) {
            let point = gestureRecognizer.location(in: self)
            let color = getColorAtPoint(point: point)
            self.delegate?.HSBColorColorPickerTouched(sender: self, color: color, point: point, state:gestureRecognizer.state)
        }        
    }
}
1
On

I quickly added code to implement a color picker for a ViewController using Apples color picker released in IOS 14. Make sure your deployment info is at least IOS 14.0 or better. See https://developer.apple.com/documentation/uikit/uicolorpickerviewcontroller for reference.

Add the color picker delegate to the class, and declare a color picker object :

class ViewController: UIViewController, UIColorPickerViewControllerDelegate  {
    
    let colorPicker = UIColorPickerViewController()

at the very end of viewDidLoad, I set the colorPicker delegate to self

        colorPicker.delegate = self

    } // ends viewDidLoad

I use a single color picker to choose colors for several different objects. When I present the color picker to the user, I set a boolean flag to true to indicate the reason why the color picker is being displayed.

        resetAllColorChangeFlags() // First make sure all the booleans are false for robust design

        changingScreenBackgroundColor = true

        present(colorPicker, animated: true, completion: nil)

I added the APIs to handle colorPickerViewControllerDidSelectColor and colorPickerViewControllerDidFinish

colorPickerViewControllerDidSelectColor checks the boolean flags, and sets a color property for the appropriate object. If the color is being applied to a border color in a layer, cgColor is used.

    func colorPickerViewControllerDidSelectColor(_ viewController: UIColorPickerViewController) {
        
        if changingScreenBackgroundColor
        {
            self.view.backgroundColor = viewController.selectedColor
        }
        if addingATintCircle
        {
            pointerToThisTintView.backgroundColor = viewController.selectedColor
        }
        if changingTextBackgroundColor
        {
            pointerToTextObjectSelected.backgroundColor = viewController.selectedColor
        }
        if changingTextColor
        {
            pointerToTextObjectSelected.textColor = viewController.selectedColor
        }
        if changingTextBorderColor
        {
            pointerToTextObjectSelected.layer.borderColor = viewController.selectedColor.cgColor
        }
        if changingPhotoBorderColor
        {
            pointerToPhotoObjectSelected.layer.borderColor = viewController.selectedColor.cgColor
        }
        if changingCameraBorderColor {
            cameraView.layer.borderColor = viewController.selectedColor.cgColor
        }
    } // ends colorPickerViewControllerDidSelectColor

colorPickerViewControllerDidFinish is used just to reset all of my booleans flags that indicate why the color picker has been presented to the user.


    func colorPickerViewControllerDidFinish(_ viewController: UIColorPickerViewController)
    {
        resetAllColorChangeFlags()

    } // ends colorPickerViewControllerDidFinish

Here is my reset routine :

    func resetAllColorChangeFlags()
    {
        changingFunTextColor = false
        changingFunTextFirstColorForGradient = false
        changingFunTextSecondColorForGradient = false
        changingScreenBackgroundColor = false
        changingTextBackgroundColor = false
        changingTextColor = false
        changingTextBorderColor = false
        changingPhotoBorderColor = false
        changingCameraBorderColor = false
        addingATintToAPhotoObject = false
        addingATintCircle = false
        addingAColorForGradients = false
        
    } // ends resetAllColorChangeFlags
0
On

Thanks for the starting point.

I took it from there and wrote a complte Color PickerViewController with a custom UIView and some drawing code.

I made the custom UIView @IBDesignable so it can be rendered in InterfaceBuilder.

https://github.com/Christian1313/iOS_Swift_ColorPicker

0
On

using Michael Ros answer,

If you want to use this view by objective-c viewcontroller you can simply create a new swift file called ColorPickerView and add a uiview to your viewcontroller on the storyboard and select ColorPickerView as it's class name. then make your view controller a notification observer of name @"colorIsPicked"

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateColor) name:@"colorIsPicked" object:nil];

Code for ColorPickerView.swift

class ColorPickerView : UIView {

@objc public lazy var onColorDidChange: ((_ color: UIColor) -> ()) = {
           
    //send a notification for the caller view to update its elements if necessery
    NotificationCenter.default.post(name: Notification.Name("colorIsPicked"), object: nil)

}


let saturationExponentTop:Float = 2.0
let saturationExponentBottom:Float = 1.3

let grayPaletteHeightFactor: CGFloat = 0.1
var rect_grayPalette = CGRect.zero
var rect_mainPalette = CGRect.zero

// adjustable
var elementSize: CGFloat = 10.0 {
    didSet {
        setNeedsDisplay()
    }
}

override init(frame: CGRect) {
    super.init(frame: frame)
    setup()
}

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    setup()
}

private func setup() {
    
    self.clipsToBounds = true
    let touchGesture = UILongPressGestureRecognizer(target: self, action: #selector(self.touchedColor(gestureRecognizer:)))
    touchGesture.minimumPressDuration = 0
    touchGesture.allowableMovement = CGFloat.greatestFiniteMagnitude
    self.addGestureRecognizer(touchGesture)
}



override func draw(_ rect: CGRect) {
    let context = UIGraphicsGetCurrentContext()
    
    rect_grayPalette = CGRect(x: 0, y: 0, width: rect.width, height: rect.height * grayPaletteHeightFactor)
    rect_mainPalette = CGRect(x: 0, y: rect_grayPalette.maxY,
                              width: rect.width, height: rect.height - rect_grayPalette.height)
    
    // gray palette
    for y in stride(from: CGFloat(0), to: rect_grayPalette.height, by: elementSize) {
        
        for x in stride(from: (0 as CGFloat), to: rect_grayPalette.width, by: elementSize) {
            let hue = x / rect_grayPalette.width
            
            let color = UIColor(white: hue, alpha: 1.0)
            
            context!.setFillColor(color.cgColor)
            context!.fill(CGRect(x:x, y:y, width:elementSize, height:elementSize))
        }
    }
    
    // main palette
    for y in stride(from: CGFloat(0), to: rect_mainPalette.height, by: elementSize) {
        
        var saturation = y < rect_mainPalette.height / 2.0 ? CGFloat(2 * y) / rect_mainPalette.height : 2.0 * CGFloat(rect_mainPalette.height - y) / rect_mainPalette.height
        saturation = CGFloat(powf(Float(saturation), y < rect_mainPalette.height / 2.0 ? saturationExponentTop : saturationExponentBottom))
        let brightness = y < rect_mainPalette.height / 2.0 ? CGFloat(1.0) : 2.0 * CGFloat(rect_mainPalette.height - y) / rect_mainPalette.height
        
        for x in stride(from: (0 as CGFloat), to: rect_mainPalette.width, by: elementSize) {
            let hue = x / rect_mainPalette.width
            
            let color = UIColor(hue: hue, saturation: saturation, brightness: brightness, alpha: 1.0)
            
            context!.setFillColor(color.cgColor)
            context!.fill(CGRect(x:x, y: y + rect_mainPalette.origin.y,
                                 width: elementSize, height: elementSize))
        }
    }
}



func getColorAtPoint(point: CGPoint) -> UIColor
{
    var roundedPoint = CGPoint(x:elementSize * CGFloat(Int(point.x / elementSize)),
                               y:elementSize * CGFloat(Int(point.y / elementSize)))
    
    let hue = roundedPoint.x / self.bounds.width
    
    
    // main palette
    if rect_mainPalette.contains(point)
    {
        // offset point, because rect_mainPalette.origin.y is not 0
        roundedPoint.y -= rect_mainPalette.origin.y
        
        var saturation = roundedPoint.y < rect_mainPalette.height / 2.0 ? CGFloat(2 * roundedPoint.y) / rect_mainPalette.height
            : 2.0 * CGFloat(rect_mainPalette.height - roundedPoint.y) / rect_mainPalette.height
        
        saturation = CGFloat(powf(Float(saturation), roundedPoint.y < rect_mainPalette.height / 2.0 ? saturationExponentTop : saturationExponentBottom))
        let brightness = roundedPoint.y < rect_mainPalette.height / 2.0 ? CGFloat(1.0) : 2.0 * CGFloat(rect_mainPalette.height - roundedPoint.y) / rect_mainPalette.height
        
        return UIColor(hue: hue, saturation: saturation, brightness: brightness, alpha: 1.0)
    }
    // gray palette
    else{
        
        return UIColor(white: hue, alpha: 1.0)
    }
}


@objc func touchedColor(gestureRecognizer: UILongPressGestureRecognizer){
    let point = gestureRecognizer.location(in: self)
    let color = getColorAtPoint(point: point)
    
    self.onColorDidChange(color)
}
 }
6
On

Based on Joel Teply code (Swift 4), with gray bar on top:

import UIKit


class ColorPickerView : UIView {

var onColorDidChange: ((_ color: UIColor) -> ())?

let saturationExponentTop:Float = 2.0
let saturationExponentBottom:Float = 1.3

let grayPaletteHeightFactor: CGFloat = 0.1
var rect_grayPalette = CGRect.zero
var rect_mainPalette = CGRect.zero

// adjustable
var elementSize: CGFloat = 1.0 {
    didSet {
        setNeedsDisplay()
    }
}

override init(frame: CGRect) {
    super.init(frame: frame)
    setup()
}

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    setup()
}

private func setup() {

    self.clipsToBounds = true
    let touchGesture = UILongPressGestureRecognizer(target: self, action: #selector(self.touchedColor(gestureRecognizer:)))
    touchGesture.minimumPressDuration = 0
    touchGesture.allowableMovement = CGFloat.greatestFiniteMagnitude
    self.addGestureRecognizer(touchGesture)
}



override func draw(_ rect: CGRect) {
    let context = UIGraphicsGetCurrentContext()

    rect_grayPalette = CGRect(x: 0, y: 0, width: rect.width, height: rect.height * grayPaletteHeightFactor)
    rect_mainPalette = CGRect(x: 0, y: rect_grayPalette.maxY,
                              width: rect.width, height: rect.height - rect_grayPalette.height)

    // gray palette
    for y in stride(from: CGFloat(0), to: rect_grayPalette.height, by: elementSize) {

        for x in stride(from: (0 as CGFloat), to: rect_grayPalette.width, by: elementSize) {
            let hue = x / rect_grayPalette.width

            let color = UIColor(white: hue, alpha: 1.0)

            context!.setFillColor(color.cgColor)
            context!.fill(CGRect(x:x, y:y, width:elementSize, height:elementSize))
        }
    }

    // main palette
    for y in stride(from: CGFloat(0), to: rect_mainPalette.height, by: elementSize) {

        var saturation = y < rect_mainPalette.height / 2.0 ? CGFloat(2 * y) / rect_mainPalette.height : 2.0 * CGFloat(rect_mainPalette.height - y) / rect_mainPalette.height
        saturation = CGFloat(powf(Float(saturation), y < rect_mainPalette.height / 2.0 ? saturationExponentTop : saturationExponentBottom))
        let brightness = y < rect_mainPalette.height / 2.0 ? CGFloat(1.0) : 2.0 * CGFloat(rect_mainPalette.height - y) / rect_mainPalette.height

        for x in stride(from: (0 as CGFloat), to: rect_mainPalette.width, by: elementSize) {
            let hue = x / rect_mainPalette.width

            let color = UIColor(hue: hue, saturation: saturation, brightness: brightness, alpha: 1.0)

            context!.setFillColor(color.cgColor)
            context!.fill(CGRect(x:x, y: y + rect_mainPalette.origin.y,
                                 width: elementSize, height: elementSize))
        }
    }
}



func getColorAtPoint(point: CGPoint) -> UIColor
{
    var roundedPoint = CGPoint(x:elementSize * CGFloat(Int(point.x / elementSize)),
                               y:elementSize * CGFloat(Int(point.y / elementSize)))

    let hue = roundedPoint.x / self.bounds.width


    // main palette
    if rect_mainPalette.contains(point)
    {
        // offset point, because rect_mainPalette.origin.y is not 0
        roundedPoint.y -= rect_mainPalette.origin.y

        var saturation = roundedPoint.y < rect_mainPalette.height / 2.0 ? CGFloat(2 * roundedPoint.y) / rect_mainPalette.height
            : 2.0 * CGFloat(rect_mainPalette.height - roundedPoint.y) / rect_mainPalette.height

        saturation = CGFloat(powf(Float(saturation), roundedPoint.y < rect_mainPalette.height / 2.0 ? saturationExponentTop : saturationExponentBottom))
        let brightness = roundedPoint.y < rect_mainPalette.height / 2.0 ? CGFloat(1.0) : 2.0 * CGFloat(rect_mainPalette.height - roundedPoint.y) / rect_mainPalette.height

        return UIColor(hue: hue, saturation: saturation, brightness: brightness, alpha: 1.0)
    }
    // gray palette
    else{

        return UIColor(white: hue, alpha: 1.0)
    }
}


@objc func touchedColor(gestureRecognizer: UILongPressGestureRecognizer){
    let point = gestureRecognizer.location(in: self)
    let color = getColorAtPoint(point: point)

    self.onColorDidChange?(color)
  }
}

Usage:

    let colorPickerView = ColorPickerView()
    colorPickerView.onColorDidChange = { [weak self] color in
        DispatchQueue.main.async {

            // use picked color for your needs here...
            self?.view.backgroundColor = color
        }

    }

    // add it to some view and set constraints
    ...