7

I want to put a gradient background in all my views.

The way that I thought to do this without the need to create a IBOutlet of a view in all view controllers is to create a new class from UIView, and inside the draw() I put the code that makes a CAGradientLayer in the view.

So, in the interface builder I just need to select the background view and set its class as my custom class. It works so far.

My question is if I can do that without problems. Somebody knows is it ok? Because the model file that inherit from UIView come with the comment: //Only override draw() if you perform custom drawing.

And I don't know if create a layer counts. Or if draw() is only to low level drawing or something like that. Do not know nothing about the best usage of the draw() func.

This is the code:

override func draw(_ rect: CGRect) {

        let layer = CAGradientLayer()
        layer.frame = CGRect(x: 0, y: 0, width: self.frame.width, height: self.frame.height)

        let color1 = UIColor(red: 4/255, green: 39/255, blue: 105/255, alpha: 1)
        let color2 = UIColor(red: 1/255, green: 91/255, blue: 168/255, alpha: 1)

        layer.colors = [color1.cgColor, color2.cgColor]
        self.layer.addSublayer(layer)
    }
Aisenhein
  • 95
  • 1
  • 7

3 Answers3

13

You can use @IBDesignable and @IBInspectable to configure the startColor and endColor properties from the Storyboard. Use CGGradient which specifies colors and locations instead of CAGradientLayer if you want to draw in draw(_ rect:) method. The Simple Gradient class and draw(_ rect: CGRect) function would look like this

import UIKit

@IBDesignable 
class GradientView: UIView {

    @IBInspectable var startColor: UIColor = UIColor(red: 4/255, green: 39/255, blue: 105/255, alpha: 1)
    @IBInspectable var endColor: UIColor = UIColor(red: 1/255, green: 91/255, blue: 168/255, alpha: 1)

    override func draw(_ rect: CGRect) {

      let context = UIGraphicsGetCurrentContext()!
      let colors = [startColor.cgColor, endColor.cgColor]

      let colorSpace = CGColorSpaceCreateDeviceRGB()

      let colorLocations: [CGFloat] = [0.0, 1.0]

      let gradient = CGGradient(colorsSpace: colorSpace,
                                     colors: colors as CFArray,
                                  locations: colorLocations)!

      let startPoint = CGPoint.zero
      let endPoint = CGPoint(x: 0, y: bounds.height)
      context.drawLinearGradient(gradient,
                          start: startPoint,
                            end: endPoint,
                        options: [CGGradientDrawingOptions(rawValue: 0)])
    }
}

you can read more about it here and tutorial

Suhit Patil
  • 11,748
  • 3
  • 50
  • 60
  • Thank you. I think it is the closer that I was looking for. And works great. – Aisenhein Dec 18 '17 at 13:58
  • And good idea to use IBInspectable and IBDesignable, makes it perfect. – Aisenhein Dec 18 '17 at 14:04
  • Great it helped. Now you can configure values from storyboard for different views. – Suhit Patil Dec 18 '17 at 14:15
  • I get an error on colorSpace which says: "Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value". what am I doing wrong? @SuhitPatil – Am1rFT Nov 28 '18 at 11:00
  • @SuhitPatil I get 'Unexpectedly found nil while unwrapping an Optional value' due to 'options' having an empty array. I'm using Swift 5. – curiously77 Dec 02 '19 at 16:59
  • 2
    @curiously77 updated the code to fix the crash. Thanks. – Suhit Patil Dec 02 '19 at 17:55
  • @SuhitPatil Is it because of Swift 5? Because I have the same issue with another argument of 'CGGradient' called 'Locations'. I get this error 'Cannot convert value of type 'CGGradientDrawingOptions' to expected element type 'CGFloat'' when I substitute '[]' for '[CGGradientDrawingOptions(rawValue: 0)]'. – curiously77 Dec 02 '19 at 19:07
  • 1
    @curiously77 not sure it's because of Swift 5. in drawLinearGradient method the options are declared as an enum of the type OptionSetType. So you pass here CGGradientDrawingOptions enum type or set containing elements from the enum. We specify empty [ ] when we don't wan't to apply any options. Please check https://stackoverflow.com/questions/36323615/cannot-convert-value-of-type-int-to-expected-argument-type-cggradientdrawingopti this issue for more details. – Suhit Patil Dec 03 '19 at 06:12
  • 1
    Plus I have shared the tutorial and documentation links as well, please check in case if you have any doubts. Try out the above code in playground. – Suhit Patil Dec 03 '19 at 06:13
  • Short and simple. Found it two days later. Youtuber's want to make it super complicated with 100s of overriden methods and tools etc. Sad how much easier Android dev is. –  Jul 24 '20 at 01:24
1

You shouldn't be adding a CAGradientLayer inside the draw function.

If you want to add a gradient to a view then the easiest way is to do it with a layer (like you are doing) but not like this...

You should add it to your view as part of the init method and then update its frame in the layout subviews method.

Something like this...

class MyView: UIView {
    let gradientLayer: CAGradientLayer = {
        let l = CAGradientLayer()
        let color1 = UIColor(red: 4/255, green: 39/255, blue: 105/255, alpha: 1)
        let color2 = UIColor(red: 1/255, green: 91/255, blue: 168/255, alpha: 1)
        l.colors = [color1.cgColor, color2.cgColor]
        return l
    }()

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

        layer.addSublayer(gradientLayer)
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        gradientLayer.frame = bounds
    }
}
Fogmeister
  • 76,236
  • 42
  • 207
  • 306
  • Thanks for the answer. But with this code, the gradient aren't showed. I just have a white background (the color of my view itself). I need to call the init(frame) some where? I think it is never called. The xcode require to me put a init(coder), may I call the init(frame) inside its? – Aisenhein Dec 18 '17 at 13:44
  • if you want to support IB/storyboards then you'll need to support both `init(frame:)` and `init(coder:)` - i typically have a function called `setup()` that is called by both initializers. – Jason Moore Oct 07 '19 at 13:08
1

I would create UIView extension and apply it when needed.

extension UIView {
        func withGradienBackground(color1: UIColor, color2: UIColor, color3: UIColor, color4: UIColor) {
            let layerGradient = CAGradientLayer()

            layerGradient.colors = [color1.cgColor, color2.cgColor, color3.cgColor, color4.cgColor]
            layerGradient.frame = bounds
            layerGradient.startPoint = CGPoint(x: 1.5, y: 1.5)
            layerGradient.endPoint = CGPoint(x: 0.0, y: 0.0)
            layerGradient.locations = [0.0, 0.3, 0.4, 1.0]

            layer.insertSublayer(layerGradient, at: 0)
        }
}

Here you can change func declaration to specify startPoint, endPointand locations params as variables.

After that just call this method like

gradienView.withGradienBackground(color1: UIColor.green, color2: UIColor.red, color3: UIColor.blue, color4: UIColor.white)

P.S. Please note that you can have as much colors as you want in colors array

mihatel
  • 846
  • 14
  • 34
  • But with this extension I will need to create a outlet of the view in all view controller to call the gradient func. i want to avoid that. Thanks anyway. – Aisenhein Dec 18 '17 at 13:56