mlevi
mlevi

Reputation: 1463

Converting a CGGradient to a CAGradientLayer

I'm using a PaintCode StyleKit to generate a bunch of gradients, but PaintCode exports them as a CGGradient. I wanted to add these gradients a layer. Is it possible to convert a CGGradient to a CAGradientLayer?

Upvotes: 2

Views: 1322

Answers (3)

TheCodePig
TheCodePig

Reputation: 122

This can be important if you have a library of gradients and only occasionally need to use a gradient in one of the two formats.

Yes, it is possible, but requires some math to convert from a regular coordinate system (with x values from 0 to the width and y values from 0 to the height) to the coordinate system used by CAGradientLayer (with x values from 0 to 1 and y values from 0 to 1). And it requires some more math (quite complex) to get the slope right.

The distance from 0 to 1 for x will depend on the width of the original rectangle. And the distance from 0 to 1 for y will depend on the height of the original rectangle. So:

let convertedStartX = startX/Double(width)
let convertedStartY = startY/Double(height)
let convertedEndX = endX/Double(width)
let convertedEndY = endY/Double(height)

let intermediateStartPoint = CGPoint(x:convertedStartX,y:convertedStartY)
let intermediateEndPoint = CGPoint(x:convertedEndX,y:convertedEndY)

This works if your original rectangle was a square. If not, the slope of the line that defines the angle of the gradient will be wrong! To fix this see the excellent answer here: CAGradientLayer diagonal gradient

If you pick up the utility from there, then you can set your final converted start and end points as follows, starting with the already-adjusted point values from above:

let fixedStartEnd:(CGPoint,CGPoint) = LinearGradientFixer.fixPoints(start: intermediateStartPoint, end: intermediateEndPoint, bounds: CGSize(width:width,height:height))

let myGradientLayer = CAGradientLayer()
myGradientLayer.startPoint = fixedStartEnd.0
myGradientLayer.endPoint = fixedStartEnd.1

Here's code for a full Struct that you can use to store gradient data and get back CGGradients or CAGradientLayers as needed:

import UIKit

struct UniversalGradient {

    //Doubles are more precise than CGFloats for the
    //calculations needed to convert start and end
    //to CAGradientLayers 1...0 format

    var startX: Double
    var startY: Double
    var endX: Double
    var endY: Double

    let options: CGGradientDrawingOptions = [.drawsBeforeStartLocation, .drawsAfterEndLocation]

    //for CAGradientLayer
    var colors: [UIColor]
    var locations: [Double]

    //computed conversions
    private var myCGColors: [CGColor] {
        return self.colors .map {color in color.cgColor}
    }

    private var myCGFloatLocations: [CGFloat] {
        return self.locations .map {location in CGFloat(location)}
    }

    //computed properties
    var gradient: CGGradient {
        return CGGradient(colorsSpace: nil, colors: myCGColors as CFArray, locations: myCGFloatLocations)!
    }

    var start: CGPoint {
        return CGPoint(x: startX,y: startY)
    }
    var end: CGPoint {
        return CGPoint(x: endX,y: endY)
    }

    //can't use computed property here
    //since we need details of the specific environment's bounds to be passed in
    //so this will be an instance function

    func gradientLayer(width: CGFloat, height: CGFloat) -> CAGradientLayer {

        //convert location x and y values from full coordinates to 0...1 for start and end
        //this works great for position, but it gets the slope incorrect if the view is not square

        //this is because the gradient is not drawn with the final scale
        //it is drawn while the view is square, and then it gets stretched, changing the angle
        //https://stackoverflow.com/questions/38821631/cagradientlayer-diagonal-gradient

        let convertedStartX = startX/Double(width)
        let convertedStartY = startY/Double(height)
        let convertedEndX = endX/Double(width)
        let convertedEndY = endY/Double(height)

        let intermediateStartPoint = CGPoint(x:convertedStartX,y:convertedStartY)
        let intermediateEndPoint = CGPoint(x:convertedEndX,y:convertedEndY)

        let fixedStartEnd:(CGPoint,CGPoint) = LinearGradientFixer.fixPoints(start: intermediateStartPoint, end: intermediateEndPoint, bounds: CGSize(width:width,height:height))

        let myGradientLayer = CAGradientLayer()
        myGradientLayer.startPoint = fixedStartEnd.0
        myGradientLayer.endPoint = fixedStartEnd.1

        myGradientLayer.locations = self.locations .map {location in NSNumber(value: location)}
        myGradientLayer.colors = self.colors .map {color in color.cgColor}

        return myGradientLayer
    }

}

Upvotes: 1

matt
matt

Reputation: 535118

No. The point of a CAGradientLayer is that you get to describe to it the gradient you want and it draws the gradient for you. You are already past that step; you have already described the gradient you want (to PaintCode instead of to a CAGradientLayer) and thus you already have the gradient you want. Thus, it is silly for you even to want to use a CAGradientLayer, since if you were going to do that, why did you use PaintCode in the first place? Just draw the CGGradient, itself, into an image, a view, or even a layer.

Upvotes: 3

jtbandes
jtbandes

Reputation: 118671

You can't get the colors out of a CGGradient, but you can use the same values to set the CAGradientLayer's colors and locations properties. Perhaps it would help for you to modify the generated PCGradient class to keep the colors and locations around as NSArrays that you can pass into CAGradientLayer.

Upvotes: 3

Related Questions