32

I have been trying to produce a basic radial gradient background, but without success. I managed to get a linear gradient working as shown with the code below, but I have no idea how to make it radial with different colours - like in the image below. Any help would be greatly appreciated. :)

    let gradientLayer: CAGradientLayer = CAGradientLayer()
    gradientLayer.colors = gradientColors
    gradientLayer.locations = gradientLocations ...

enter image description here

enter image description here

Fattie
  • 27,874
  • 70
  • 431
  • 719
Gugulethu
  • 1,426
  • 3
  • 18
  • 36
  • 2
    if you are doing this way for learning purpose thats fine but if you are using this for design purpose I recommend to design it in sketch3 and use it as UIImage – sriram hegde Aug 06 '15 at 11:03
  • This http://stackoverflow.com/questions/16043534/circular-cagradientlayer-mask may help you. – RayChen Aug 06 '15 at 11:05
  • @sriramhegde The background needs to dynamically Change. An image wouldn't help. Thanks. – Gugulethu Aug 06 '15 at 16:54
  • Please note on this old question, **this is now very easy** - answer down below! – Fattie Apr 12 '20 at 15:42
  • (obviously never use an image for a gradient in an iPhone app! the gpu is incredibly good at making gradients; an image will look horrible, won't scale, and is very non-performant.) – Fattie Apr 12 '20 at 17:33

5 Answers5

34

Nowadays CAGradientLayer is built-in to iOS.

It's this easy:

For years now you simply do this:

class GlowBall: UIView {
    private lazy var pulse: CAGradientLayer = {
        let l = CAGradientLayer()
        l.type = .radial
        l.colors = [ UIColor.red.cgColor,
            UIColor.yellow.cgColor,
            UIColor.green.cgColor,
            UIColor.blue.cgColor]
        l.locations = [ 0, 0.3, 0.7, 1 ]
        l.startPoint = CGPoint(x: 0.5, y: 0.5)
        l.endPoint = CGPoint(x: 1, y: 1)
        layer.addSublayer(l)
        return l
    }()

    override func layoutSubviews() {
        super.layoutSubviews()
        pulse.frame = bounds
        pulse.cornerRadius = bounds.width / 2.0
    }

}
    

enter image description here

The key lines are:

l.colors = [ UIColor.red.cgColor,
                UIColor.yellow.cgColor,
                UIColor.green.cgColor,
                UIColor.blue.cgColor]
l.locations = [ 0, 0.3, 0.7, 1 ]
    

Note that you can change the "stretch" as you wish ...

l.locations = [ 0, 0.1, 0.2, 1 ]
    
    

enter image description here

Use any colors you like

l.colors = [ UIColor.systemBlue.cgColor,
                    UIColor.systemPink.cgColor,
                    UIColor.systemBlue.cgColor,
                    UIColor.systemPink.cgColor,
                    UIColor.systemBlue.cgColor,
                    UIColor.systemPink.cgColor,
                    UIColor.systemBlue.cgColor,
                    UIColor.systemPink.cgColor]
                l.locations = [ 0,0.1,0.2,0.3,0.4,0.5,0.6,1 ]
    

enter image description here

It's really that easy now.

Very useful trick:

Say you want yellow, with a blue band at 0.6:

l.colors = [ UIColor.yellow.cgColor,
                    UIColor.blue.cgColor,
                    UIColor.yellow.cgColor]
                l.locations = [ 0, 0.6, 1 ]
    

That works fine.

    # yellow...
    # blue...
    # yellow...
    

But usually you do this:

    # yellow...
    # yellow...
    # blue...
    # yellow...
    # yellow...
    

Notice there are TWO of the yellows at each end ...

l.colors = [ UIColor.yellow.cgColor,
 UIColor.yellow.cgColor,
 UIColor.blue.cgColor,
 UIColor.yellow.cgColor,
 UIColor.yellow.cgColor]
    

In this way, you can control "how wide" the blue band is:

In this example: the blue band will be narrow and sharp:

l.locations = [ 0, 0.58, 0.6, 0.68, 1 ]
    

In this example the blue band will be broad and soft:

l.locations = [ 0, 0.5, 0.6, 0.7, 1 ]
    

That is really the secret to how you control gradients, and get the look you want.


How to use ...

Notice this is - very simply - a UIView !!

class GlowBall: UIView { ...

Thus simply

  1. In storyboard, place a UIView where you want

  2. In storyboard, change the class to "GlowBall" instead of UIView

You're done!

Fattie
  • 27,874
  • 70
  • 431
  • 719
23

Here is an implementation in Swift 3 if you're just looking for a UIView radial gradient background:

class RadialGradientLayer: CALayer {

    var center: CGPoint {
        return CGPoint(x: bounds.width/2, y: bounds.height/2)
    }

    var radius: CGFloat {
        return (bounds.width + bounds.height)/2
    }

    var colors: [UIColor] = [UIColor.black, UIColor.lightGray] {
        didSet {
            setNeedsDisplay()
        }
    }

    var cgColors: [CGColor] {
        return colors.map({ (color) -> CGColor in
            return color.cgColor
        })
    }

    override init() {
        super.init()
        needsDisplayOnBoundsChange = true
    }

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

    override func draw(in ctx: CGContext) {
        ctx.saveGState()
        let colorSpace = CGColorSpaceCreateDeviceRGB()
        let locations: [CGFloat] = [0.0, 1.0]
        guard let gradient = CGGradient(colorsSpace: colorSpace, colors: cgColors as CFArray, locations: locations) else {
            return
        }
        ctx.drawRadialGradient(gradient, startCenter: center, startRadius: 0.0, endCenter: center, endRadius: radius, options: CGGradientDrawingOptions(rawValue: 0))
    }

}



class RadialGradientView: UIView {

    private let gradientLayer = RadialGradientLayer()

    var colors: [UIColor] {
        get {
            return gradientLayer.colors
        }
        set {
            gradientLayer.colors = newValue
        }
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        if gradientLayer.superlayer == nil {
            layer.insertSublayer(gradientLayer, at: 0)
        }
        gradientLayer.frame = bounds
    }

}
Kurt J
  • 2,558
  • 24
  • 17
  • Neat solution, + for easy extendibility. – inokey Apr 04 '18 at 10:10
  • How to use this in a Playground? – Petrus Theron Aug 01 '18 at 11:09
  • For anyone googling here, please note that `CAGradientLayer` now exists. You definitely don't need to `draw` these days. It's completely built-in. (As a detail, I believe this will crash if the array sent in has a number other than 2 items.) – Fattie May 06 '21 at 11:10
21

Have a look at my implementation of RadialGradientLayer, and feel free to modify it

class RadialGradientLayer: CALayer {

   override init(){

        super.init()

        needsDisplayOnBoundsChange = true
    }

     init(center:CGPoint,radius:CGFloat,colors:[CGColor]){

        self.center = center
        self.radius = radius
        self.colors = colors

        super.init()

    }

    required init(coder aDecoder: NSCoder) {

        super.init()

    }

    var center:CGPoint = CGPointMake(50,50)
    var radius:CGFloat = 20
    var colors:[CGColor] = [UIColor(red: 251/255, green: 237/255, blue: 33/255, alpha: 1.0).CGColor , UIColor(red: 251/255, green: 179/255, blue: 108/255, alpha: 1.0).CGColor]

    override func drawInContext(ctx: CGContext!) {

        CGContextSaveGState(ctx)

        var colorSpace = CGColorSpaceCreateDeviceRGB()

        var locations:[CGFloat] = [0.0, 1.0]

        var gradient = CGGradientCreateWithColors(colorSpace, colors, [0.0,1.0])

        var startPoint = CGPointMake(0, self.bounds.height)
        var endPoint = CGPointMake(self.bounds.width, self.bounds.height)

        CGContextDrawRadialGradient(ctx, gradient, center, 0.0, center, radius, 0)

    }

}

In my case I needed it with two colors only and if you need more colors you need to modify location array declared in drawInContext. Also after creating object from this class don't forget to call its setNeedsDisplay() otherwise it wont work. Also sometimes I needed different size gradients so thats why you have to pass radius parameter in initializer and the center point of your gradient

Zell B.
  • 10,266
  • 3
  • 40
  • 49
  • This is how I created the object in the viewDidLoad but It did not work for one reason or the other. @zellb. var screenCentre = CGPointMake(100, 100) let innerColour = UIColor.redColor() as! CGColor let outterColour = UIColor.greenColor() as! CGColor var radialGradientBackground = RadialGradientLayer(center: screenCentre, radius: CGFloat(50.0), colors: [innerColour, outterColour]) radialGradientBackground.frame = self.view!.bounds self.view!.layer.insertSublayer(radialGradientBackground, atIndex: 0) self.view!.setNeedsDisplay() – Gugulethu Aug 06 '15 at 17:00
  • 1
    setNeedsDisplay must be called on radialGradientBackground and not in view – Zell B. Aug 06 '15 at 19:56
  • Sweet. Worked well @zellb. Thanks. ;) – Gugulethu Aug 07 '15 at 02:42
  • @Gugulethu glad to hear that – Zell B. Aug 07 '15 at 09:43
  • I have an issue with this one, if you add it in viewDidLoad, then you can notice the layer being added to view, which causes a glitch and delay of the dispaly. any thoughts of how to avoid it happenning? – rcat24 Apr 27 '16 at 13:15
  • 1
    locations, startPoint and endPoint are not even used in your drawInContext – Leo Dabus Oct 03 '16 at 17:57
  • In total, this is how to make this work: 1. Construct layer: `let layer = RadialGradientLayer(center: centerPoint, radius: radius, colors: [UIColor.black.cgColor, UIColor.white.cgColor])` 2. `layer.setNeedsDisplay()` 3. `layer.frame = self.view.bounds` 4. `self.view.layer.insertSublayer(layer, at: 0)` Thank you!! – levibostian Mar 15 '17 at 16:42
  • @ZellB. no need to call `setNeedsDisplay` if you put `needsDisplayOnBoundsChange = true` within your own `init(center:...)` method instead of putting it in `override init()`. – Santosh Jul 25 '17 at 18:09
  • Really, you now do this **with a simple gradient layer**, you'd never `draw` it these days. For anyone googling here it's important to note that really old QA like this go out of date! Simply use a gradient layer. – Fattie Apr 12 '20 at 17:31
9
@IBDesignable class RadialGradientView: UIView {

    @IBInspectable var outsideColor: UIColor = UIColor.red
    @IBInspectable var insideColor: UIColor = UIColor.green

    override func draw(_ rect: CGRect) {
        let colors = [insideColor.cgColor, outsideColor.cgColor] as CFArray
        let endRadius = sqrt(pow(frame.width/2, 2) + pow(frame.height/2, 2))
        let center = CGPoint(x: bounds.size.width / 2, y: bounds.size.height / 2)
        let gradient = CGGradient(colorsSpace: nil, colors: colors, locations: nil)
        let context = UIGraphicsGetCurrentContext()

        context?.drawRadialGradient(gradient!, startCenter: center, startRadius: 0.0, endCenter: center, endRadius: endRadius, options: CGGradientDrawingOptions.drawsBeforeStartLocation)
    }
}

See my full answer here.

Community
  • 1
  • 1
ChikabuZ
  • 10,031
  • 5
  • 63
  • 86
1

A little different approach with function which takes parent view, colors, and locations as input. The function returns a subview where the layer was added. This gives the flexibility to hide/show/remove subview.

enter image description here

override func viewDidLoad() {
    super.viewDidLoad()
    //squareView is my parent view I am going to add gradient view to it
    squareView.backgroundColor = UIColor.black
    //Add CG colors
    let colours = [UIColor.red.cgColor,UIColor.green.cgColor,UIColor.clear.cgColor]
    //Add location with same count as colors, these describe contribution in gradient from center 0 to end 1
    let locations:[NSNumber] = [0,0.6,0.8]
    //Use gradientView reference to show/hide, remove/re-add from view
    let gradientView = self.addGradientViewTo(parentView: self.squareView, colors:colours,locations: locations)
}

func addGradientViewTo (parentView:UIView,colors:[CGColor],locations:[NSNumber]) -> UIView  {
    //Create customGradientView with exact dimension of parent, add it with centering with parent
    let customGradientView = UIView()
    customGradientView.backgroundColor = UIColor.clear
    customGradientView.frame = parentView.bounds
    parentView.addSubview(customGradientView)
    customGradientView.centerXAnchor.constraint(equalTo: parentView.centerXAnchor).isActive = true
    customGradientView.centerYAnchor.constraint(equalTo: parentView.centerYAnchor).isActive = true
    parentView.clipsToBounds = true

    //Create layer add it to customGradientView
    let gradientLayer = CAGradientLayer()
    gradientLayer.type = .radial //Circular
    gradientLayer.opacity = 0.8
    gradientLayer.colors = colors
    gradientLayer.locations = locations
    gradientLayer.frame = customGradientView.bounds
    
    //Set start point as center and radius as 1, co-ordinate system maps 0 to 1, 0,0 top left, bottom right 1,1
    gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.5)
    let radius = 1.0
    gradientLayer.endPoint = CGPoint(x: radius, y: radius)
          
    //Add layer at top to make sure its visible
    let layerCount:UInt32 = UInt32(customGradientView.layer.sublayers?.count ?? 0)
    customGradientView.layer.insertSublayer(gradientLayer, at: layerCount)
    customGradientView.layoutIfNeeded()
    
    //Use reference to show/hide add/remove gradient view
    return customGradientView
}
Aditya Deshmane
  • 4,676
  • 2
  • 29
  • 35