skip to Main Content

I am using CAGradientLayer to add a gradient to my UIView. I am setting the gradient ad a solid red colourful testing, and when I check the color that is being displayed, it is showing different RGB values to those I have specified. Is there a way that I can make sure the gradient is showing the colours that I set.

let gradientLayer2 = CAGradientLayer()
gradientLayer2.frame.size = colourPickerView.frame.size
gradientLayer2.colors = [
    UIColor(red: 1.00, green: 0.00, blue: 0.00, alpha: 1.00).cgColor
    UIColor(red: 1.00, green: 0.00, blue: 0.00, alpha: 1.00).cgColor,
]
    
gradientLayer2.startPoint = CGPoint(x: 0.0, y: 0.0)
gradientLayer2.endPoint = CGPoint(x: 1, y: 0)
gradientLayer2.cornerRadius = 20
colourPickerView.layer.insertSublayer(gradientLayer2, at: 0)

This shows the red color as: <CGColor 0x283c8e400> [<CGColorSpace 0x283c989c0> (kCGColorSpaceICCBased; kCGColorSpaceModelRGB; sRGB IEC61966-2.1; extended range)] ( 1 0.14902 0 1 )
(Please note, it is showing the green value as ‘0.14902’)

When I add a solid background colour to the layer, instead of a gradient, it does show the correct RGB values.

let layer = CALayer()
layer.frame.size = colourPickerView.frame.size
layer.backgroundColor = UIColor(red: 1.00, green: 0.00, blue: 0.00, alpha: 1.00).cgColor
colourPickerView.layer.insertSublayer(layer, at: 0)

Here is the code that I am using to get the colour of a certain pixel:

extension UIView {
func colorOfPointView(point: CGPoint) -> UIColor {
    let colorSpace: CGColorSpace = CGColorSpaceCreateDeviceRGB()
    let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)

    var pixelData: [UInt8] = [0, 0, 0, 0]

    let context = CGContext(data: &pixelData, width: 1, height: 1, bitsPerComponent: 8, bytesPerRow: 4, space: colorSpace, bitmapInfo: bitmapInfo.rawValue)

    context!.translateBy(x: -point.x, y: -point.y)

    self.layer.render(in: context!)

    let red: CGFloat = CGFloat(pixelData[0]) / CGFloat(255.0)
    let green: CGFloat = CGFloat(pixelData[1]) / CGFloat(255.0)
    let blue: CGFloat = CGFloat(pixelData[2]) / CGFloat(255.0)
    let alpha: CGFloat = CGFloat(pixelData[3]) / CGFloat(255.0)

    let color: UIColor = UIColor(red: red, green: green, blue: blue, alpha: alpha)

    return color
}

2

Answers


  1. that’s odd! Is this the only code that involves with this part of code? I just tested and got true result :

     Optional([<CGColor 0x6000002bd6e0> [<CGColorSpace 0x6000002b9980> (kCGColorSpaceICCBased; kCGColorSpaceModelRGB; sRGB IEC61966-2.1; extended range)] ( 1 0 0 1 ), <CGColor 0x6000002bd2c0> [<CGColorSpace 0x6000002b9980> (kCGColorSpaceICCBased; kCGColorSpaceModelRGB; sRGB IEC61966-2.1; extended range)] ( 1 0 0 1 )])
    

    Copy and paste this code in a new Xcode project and check if you still get the wrong result.

    import UIKit
    
    class ViewController: UIViewController {
    
        let gradientLayer2 = CAGradientLayer()
        let slider = UISlider(frame: CGRect(x: 10, y: 400, width: 100, height: 50))
    
        override func viewDidLoad() {
            super.viewDidLoad()
            
            slider.maximumValue = 1
            slider.minimumValue = 0
            slider.addTarget(self, action: #selector(sliderChanged), for: .valueChanged)
            slider.backgroundColor = .red
            gradientLayer2.frame.size = CGSize(width: 200, height: 200)
            gradientLayer2.colors = [
                UIColor(red: 1.00, green: 0.00, blue: 0.00, alpha: 1.00).cgColor,
                UIColor(red: 1.00, green: 0.00, blue: 0.00, alpha: 1.00).cgColor
            ]
                
            gradientLayer2.startPoint = CGPoint(x: 0.0, y: 0.0)
            gradientLayer2.endPoint = CGPoint(x: 1, y: 0)
            gradientLayer2.cornerRadius = 20
            view.layer.insertSublayer(gradientLayer2, at: 0)
            view.addSubview(slider)
           
        }
    
        @objc func sliderChanged()
        {
            gradientLayer2.colors = [
                UIColor(red: 1.00, green: 0.00, blue: 0.00, alpha: 1.00).cgColor,
                UIColor(red: CGFloat(slider.value), green: 0.00, blue: 0.00, alpha: 1.00).cgColor
            ]
            print(gradientLayer2.colors)
        }
    
    }
    
    Login or Signup to reply.
  2. The issue you are running into is the difference between RGBA and Display P3 or Extended sRGBA.

    Using your approach with CGContext and a CAGradientLayer, for example, we get back the Display P3 values.

    To get the "traditional" 8-bits-per-component values, we can first render the view to a UIImage and then get the RGBA values from a point in the image.

    Using these two extensions:

    extension UIView {
        func colorAt(point: CGPoint) -> UIColor? {
            return renderView().getPixelColor(point: point)
        }
        func renderView() -> UIImage {
            let renderer = UIGraphicsImageRenderer(size: bounds.size)
            let image = renderer.image { rendererContext in
                drawHierarchy(in: bounds, afterScreenUpdates: true)
            }
            return image
        }
    }
    extension UIImage {
    
        // from: https://stackoverflow.com/a/34596653/6257435
        func getPixelColor(point: CGPoint) -> UIColor? {
            guard let cgImage = cgImage else { return nil }
            
            if point.x < 0 || point.x > size.width || point.y < 0 || point.y > size.height {
                return nil
            }
    
            let width = Int(size.width)
            let height = Int(size.height)
            let colorSpace = CGColorSpaceCreateDeviceRGB()
            
            guard let context = CGContext(data: nil,
                                          width: width,
                                          height: height,
                                          bitsPerComponent: 8,
                                          bytesPerRow: width * 4,
                                          space: colorSpace,
                                          bitmapInfo: CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue)
            else {
                return nil
            }
            
            context.draw(cgImage, in: CGRect(origin: .zero, size: size))
            
            guard let pixelBuffer = context.data else { return nil }
            
            let pointer = pixelBuffer.bindMemory(to: UInt32.self, capacity: width * height)
            let pixel = pointer[Int(point.y) * width + Int(point.x)]
            
            let r: CGFloat = CGFloat(red(for: pixel))   / 255
            let g: CGFloat = CGFloat(green(for: pixel)) / 255
            let b: CGFloat = CGFloat(blue(for: pixel))  / 255
            let a: CGFloat = CGFloat(alpha(for: pixel)) / 255
            
            return UIColor(red: r, green: g, blue: b, alpha: a)
        }
        
        private func alpha(for pixelData: UInt32) -> UInt8 {
            return UInt8((pixelData >> 24) & 255)
        }
        private func red(for pixelData: UInt32) -> UInt8 {
            return UInt8((pixelData >> 16) & 255)
        }
        private func green(for pixelData: UInt32) -> UInt8 {
            return UInt8((pixelData >> 8) & 255)
        }
        private func blue(for pixelData: UInt32) -> UInt8 {
            return UInt8((pixelData >> 0) & 255)
        }
        private func rgba(red: UInt8, green: UInt8, blue: UInt8, alpha: UInt8) -> UInt32 {
            return (UInt32(alpha) << 24) | (UInt32(red) << 16) | (UInt32(green) << 8) | (UInt32(blue) << 0)
        }
    }
    

    We can call:

    let theColor = someView.colorAt(point: CGPoint(x: 10, y: 10))
    

    and we’ll get back what we were expecting.

    Because of the need to "capture as UIImage" we don’t want to be calling that over and over – such as if we’re dragging along a gradient to get the current color… Instead, if possible, we’d want to render the view to a UIImage once and then repeatedly call prerenderedImage.getPixelColor(point: pt).

    Here’s a quick example…

    It will look like this when running:

    enter image description here enter image description here

    enter image description here enter image description here

    We have 2 CAGradientLayer views… the first one using Red -> secondColor and the second one using secondColor -> secondColor (so it appear solid).

    The 3rd view has a "left-half" CALayer and a "right-half" CALayer and the 4th view is a plain UIView with the .backgroundColor set.

    The button cycles through Red, Green, Blue, Yellow as the second-colors.

    When we touch / drag the dashed-line, we’ll get the RGBA values from the same x-coordinate on each view (at center-y) and show the results in the label below the button.

    Use the above extensions with this code…

    View Controller

    class ColorAtPointViewController: UIViewController {
    
        // CAGradientLayer from color1 to color2
        let gradientView1 = MyGradientView()
        
        // CAGradientLayer from color2 to color2 (will appear solid)
        let gradientView2 = MyGradientView()
        
        // left-half CALayer color1 / right-half CALayer color2
        let layerView = MyLayerView()
        
        // plain UIView with .backgroundColor set to color2
        let bkgView = MyBackgroundView()
    
        // a dash-line to show the touch-point
        let lineView = MyLineView()
        
        // where we'll show the RGBA values
        let outputLabel = UILabel()
    
        // button to cycle through the 2nd colors
        var btn: UIButton!
        
        // references to the 4-variations of views (for convenience)
        var views: [UIView] = []
        
        // we'll "cache" the views, rendered to UIImages
        var renderedViews: [UIImage] = []
        
        let secondColors: [UIColor] = [
            UIColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0),
            UIColor(red: 0.0, green: 1.0, blue: 0.0, alpha: 1.0),
            UIColor(red: 0.0, green: 0.0, blue: 1.0, alpha: 1.0),
            UIColor(red: 1.0, green: 1.0, blue: 0.0, alpha: 1.0),
        ]
        let secondColorNames: [String] = [
            "Red", "Green", "Blue", "Yellow",
        ]
        var c2IDX: Int = 0
        
        override func viewDidLoad() {
            super.viewDidLoad()
    
            view.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
            
            // references to the views
            views = [gradientView1, gradientView2, layerView, bkgView]
            
            let strs: [String] = [
                "CAGradientLayer View1 (red -> color2)",
                "CAGradientLayer View2 (color2 -> color2)",
                "CALayer View (left red, right color2)",
                "Background Color View (color2)"
            ]
            
            let stackView = UIStackView()
            stackView.axis = .vertical
            stackView.spacing = 4
            
            for (str, v) in zip(strs, views) {
                let label = UILabel()
                label.text = str
                v.heightAnchor.constraint(equalToConstant: 40.0).isActive = true
                stackView.addArrangedSubview(label)
                stackView.addArrangedSubview(v)
                stackView.setCustomSpacing(20.0, after: v)
            }
    
            [stackView, lineView].forEach { v in
                v.translatesAutoresizingMaskIntoConstraints = false
                view.addSubview(v)
            }
            
            let g = view.safeAreaLayoutGuide
            NSLayoutConstraint.activate([
                
                stackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
                stackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
                stackView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
                
                lineView.topAnchor.constraint(equalTo: gradientView1.topAnchor, constant: -8.0),
                lineView.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: 0.0),
                lineView.trailingAnchor.constraint(equalTo: stackView.trailingAnchor, constant: 0.0),
                lineView.bottomAnchor.constraint(equalTo: bkgView.bottomAnchor, constant: 12.0),
    
            ])
    
            var config = UIButton.Configuration.filled()
            config.buttonSize = .medium
            config.cornerStyle = .medium
            config.title = "Change 2nd Color to"
            
            btn = UIButton(configuration: config)
            btn.addAction (
                UIAction { _ in
                    self.nextColor()
                }, for: .touchUpInside
            )
    
            outputLabel.numberOfLines = 0
            outputLabel.font = .systemFont(ofSize: 13, weight: .regular)
            outputLabel.text = "RGBA at X:"
            [btn, outputLabel].forEach { v in
                v.translatesAutoresizingMaskIntoConstraints = false
                view.addSubview(v)
            }
    
            NSLayoutConstraint.activate([
                
                btn.topAnchor.constraint(equalTo: stackView.bottomAnchor, constant: 40.0),
                btn.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
                btn.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
                
                outputLabel.topAnchor.constraint(equalTo: btn.bottomAnchor, constant: 20.0),
                outputLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
                outputLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
    
            ])
            
            c2IDX = -1
            nextColor()
        }
        override func viewDidAppear(_ animated: Bool) {
            super.viewDidAppear(animated)
            lineView.x = gradientView1.bounds.midX
            gotTouch(lineView.x)
        }
        func genImages() {
            // render the views to images each time we change the colors
            //  so we don't re-render them every time we want to get a color at a point
            renderedViews = []
            views.forEach { v in
                renderedViews.append(v.renderView())
            }
        }
        
        override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
            guard let t = touches.first else { return }
            let x = t.location(in: gradientView1).x
            guard x >= 0.0, x <= gradientView1.bounds.maxX else { return }
            gotTouch(x)
        }
        override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
            guard let t = touches.first else { return }
            let x = t.location(in: gradientView1).x
            guard x >= 0.0, x <= gradientView1.bounds.maxX else { return }
            gotTouch(x)
        }
        func gotTouch(_ x: CGFloat) {
            var outputStr: String = "RGBA at X: "
            if UIScreen.main.scale == 3 {
                // if the screen scale is @3x - such as an iPhone 14 Pro - we get 1/3 points
                //  so, display as .000 or .333 or .667 instead of .666666666667
                outputStr += "(String(format: "%0.3f", x))n"
            } else {
                // @2x scale, so display as .0 or .5
                outputStr += "(String(format: "%0.1f", x))n"
            }
            let strs: [String] = ["G1", "G2", "L", "B"]
            for (str, img) in zip(strs, renderedViews) {
                let pt: CGPoint = .init(x: x, y: img.size.height * 0.5)
                if let c = img.getPixelColor(point: pt) {
                    // change "UIExtendedSRGBColorSpace 1 0 0 1" to "RGBA  1  0  0  1"
                    //  just so we can focus on the color values
                    let s = "(c)".replacingOccurrences(of: "UIExtendedSRGBColorSpace", with: "RGBA")
                        .replacingOccurrences(of: " ", with: "  ")
                    outputStr += "(str):t(s)n"
                }
            }
            lineView.x = x
            outputLabel.text = outputStr
        }
        
        func nextColor() {
            // cycle to the next "second color"
            self.c2IDX += 1
            let c = self.secondColors[self.c2IDX % self.secondColors.count]
            self.views.forEach { v in
                if let v = v as? MyBaseView {
                    v.c2 = c
                }
            }
            // gradientView2 uses color2 -> color2 to appear "solid"
            self.gradientView2.c1 = c
            
            // let the views' layers update before we render new images from the views
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: {
                // re-render the views
                self.genImages()
                // simiulate touch at last touch-point
                self.gotTouch(self.lineView.x)
                // update the button label
                let cName = self.secondColorNames[(self.c2IDX + 1) % self.secondColorNames.count]
                self.btn.configuration?.title = "Change 2nd Color to (cName)"
            })
        }
    }
    

    UIView subclasses

    class MyBaseView: UIView {
        
        var c1: UIColor = .red { didSet { colorChanged() } }
        var c2: UIColor = .red { didSet { colorChanged() } }
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        func commonInit() {
        }
        func colorChanged() {
        }
        
    }
    
    class MyGradientView: MyBaseView {
        
        let gradLayer = CAGradientLayer()
        
        override func commonInit() {
            super.commonInit()
            
            gradLayer.startPoint = CGPoint(x: 0.0, y: 0.0)
            gradLayer.endPoint = CGPoint(x: 1, y: 0)
            gradLayer.colors = [c1.cgColor, c2.cgColor]
            
            layer.addSublayer(gradLayer)
        }
        
        override func layoutSubviews() {
            super.layoutSubviews()
            gradLayer.frame = bounds
        }
        
        override func colorChanged() {
            super.colorChanged()
            CATransaction.begin()
            CATransaction.setDisableActions(true)
            gradLayer.colors = [c1.cgColor, c2.cgColor]
            CATransaction.commit()
        }
        
    }
    
    class MyLayerView: MyBaseView {
        
        let myLayer1 = CALayer()
        let myLayer2 = CALayer()
        
        override func commonInit() {
            super.commonInit()
            
            myLayer1.backgroundColor = c1.cgColor
            myLayer2.backgroundColor = c2.cgColor
            
            layer.addSublayer(myLayer1)
            layer.addSublayer(myLayer2)
        }
        
        override func layoutSubviews() {
            super.layoutSubviews()
            var r = bounds
            r.size.width *= 0.5
            myLayer1.frame = r
            r.origin.x = r.size.width
            myLayer2.frame = r
        }
        
        override func colorChanged() {
            super.colorChanged()
            CATransaction.begin()
            CATransaction.setDisableActions(true)
            myLayer1.backgroundColor = c1.cgColor
            myLayer2.backgroundColor = c2.cgColor
            CATransaction.commit()
        }
        
    }
    
    class MyBackgroundView: MyBaseView {
        
        override func commonInit() {
            super.commonInit()
            backgroundColor = c2
        }
        
        override func colorChanged() {
            super.colorChanged()
            backgroundColor = c2
        }
        
    }
    
    class MyLineView: UIView {
        
        var x: CGFloat = 0 { didSet { setNeedsLayout() } }
        
        // this allows us to use the "base" layer as a shape layer
        //  instead of adding a sublayer
        lazy var shapeLayer: CAShapeLayer = self.layer as! CAShapeLayer
        override class var layerClass: AnyClass {
            return CAShapeLayer.self
        }
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        func commonInit() {
            layer.masksToBounds = true
            shapeLayer.strokeColor = UIColor.gray.cgColor
            shapeLayer.fillColor = UIColor.clear.cgColor
            shapeLayer.lineWidth = 1
            shapeLayer.lineDashPattern = [8, 8]
        }
        override func layoutSubviews() {
            super.layoutSubviews()
            
            let bez = UIBezierPath()
            bez.move(to: .init(x: x, y: bounds.minY))
            bez.addLine(to: .init(x: x, y: bounds.maxY))
            
            CATransaction.begin()
            CATransaction.setDisableActions(true)
            shapeLayer.path = bez.cgPath
            CATransaction.commit()
        }
        
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search