How to Apply Gradient to Background View of iOS Swift App

How to Apply Gradient to background view of iOS Swift App

The Colors you're providing to gradient must be of type CGColor. So set your array of CGColor to gl.colors.

The correct code is :

class Colors {
var gl:CAGradientLayer!

init() {
let colorTop = UIColor(red: 192.0 / 255.0, green: 38.0 / 255.0, blue: 42.0 / 255.0, alpha: 1.0).cgColor
let colorBottom = UIColor(red: 35.0 / 255.0, green: 2.0 / 255.0, blue: 2.0 / 255.0, alpha: 1.0).cgColor

self.gl = CAGradientLayer()
self.gl.colors = [colorTop, colorBottom]
self.gl.locations = [0.0, 1.0]
}
}

Programmatically create a UIView with color gradient

Objective-C:

UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 50)];
CAGradientLayer *gradient = [CAGradientLayer layer];

gradient.frame = view.bounds;
gradient.colors = @[(id)[UIColor whiteColor].CGColor, (id)[UIColor blackColor].CGColor];

[view.layer insertSublayer:gradient atIndex:0];

Swift:

let view = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 50))
let gradient = CAGradientLayer()

gradient.frame = view.bounds
gradient.colors = [UIColor.white.cgColor, UIColor.black.cgColor]

view.layer.insertSublayer(gradient, at: 0)

Info: use startPoint and endPoint to change direction of gradient.

If there are any other views added onto this UIView (such as a UILabel), you may want to consider setting the background color of those UIView’s to [UIColor clearColor] so the gradient view is presented instead of the background color for sub views. Using clearColor has a slight performance hit.

Applying gradient colour to storyboard view with auto layout

Here is one way to make a @IBDesignable gradient view. It's using the default top-to-bottom gradient direction:

@IBDesignable
class MyGradientView: UIView {

@IBInspectable var color1: UIColor = .red {
didSet { setNeedsDisplay() }
}
@IBInspectable var color2: UIColor = .yellow {
didSet { setNeedsDisplay() }
}

private var gradientLayer: CAGradientLayer!

override class var layerClass: AnyClass {
return CAGradientLayer.self
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// use self.layer as the gradient layer
gradientLayer = self.layer as? CAGradientLayer
gradientLayer.colors = [color1.cgColor, color2.cgColor]
}

override func layoutSubviews() {
super.layoutSubviews()

gradientLayer.colors = [color1.cgColor, color2.cgColor]
}

}

It has @IBInspectable vars for "color1" and "color2" ... changing them in the Attributes Inspector will be reflected in Storyboard.

No need for any additional code, such as your current approach with setGradientBackground()

iOS - How to apply gradient color to UICollectionViewCell backgroundView?

You can try

override func layoutSubviews() {
super.layoutSubviews()
if !(self.layer.sublayers?.first is CAGradientLayer) {
CATransaction.begin()
CATransaction.setDisableActions(true)
UIUtils.setGradientWhite(uiView: self)
CATransaction.commit()
}
}

Change Gradient Background of UIView Through Sublayer

You can get the Gradient layer and can remove from view anywhere you want to ... Not just by adding it again you remove it..

if let gradientLayer = (yourView.layer.sublayers?.compactMap { $0 as? CAGradientLayer })?.first {

print("gradientLayer is presnet")
gradientLayer.removeFromSuperlayer()

} else {

print("its not added on layer yet")
}

How to apply gradient at back of TableView Controller?

Insert the gradient's layer to a UIView and assign it as the backgroundView of your tableView:-

let gradientLayer = CAGradientLayer()
gradientLayer.frame = yourTableView.bounds
gradientLayer.colors = [UIColor.purple.cgColor,UIColor.red.cgColor]
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
gradientLayer.endPoint = CGPoint(x: 0, y: 1)

let backgroundView = UIView(frame: yourTableView.bounds)
backgroundView.layer.insertSublayer(gradientLayer, at: 0)

yourTableView.backgroundView = backgroundView

Also make sure that your cells are .clear in colour

iOS - Draw a view with gradient background

To begin with a path needs to be generated. You probably already have this but you have not provided any code for it although you mentioned "I am able to draw the black lines in the shape". So to begin with the code...

private func generatePath(withPoints points: [CGPoint], inFrame frame: CGRect) -> UIBezierPath? {
guard points.count > 2 else { return nil } // At least 3 points
let pointsInPolarCoordinates: [(angle: CGFloat, radius: CGFloat)] = points.map { point in
let radius = (point.x*point.x + point.y*point.y).squareRoot()
let angle = atan2(point.y, point.x)
return (angle, radius)
}
let maximumPointRadius: CGFloat = pointsInPolarCoordinates.max(by: { $1.radius > $0.radius })!.radius
guard maximumPointRadius > 0.0 else { return nil } // Not all points may be centered

let maximumFrameRadius = min(frame.width, frame.height)*0.5
let radiusScale = maximumFrameRadius/maximumPointRadius

let normalizedPoints: [CGPoint] = pointsInPolarCoordinates.map { polarPoint in
.init(x: frame.midX + cos(polarPoint.angle)*polarPoint.radius*radiusScale,
y: frame.midY + sin(polarPoint.angle)*polarPoint.radius*radiusScale)
}

let path = UIBezierPath()
path.move(to: normalizedPoints[0])
normalizedPoints[1...].forEach { path.addLine(to: $0) }
path.close()
return path
}

Here points are expected to be around 0.0. They are distributed so that they try to fill maximum space depending on given frame and they are centered on it. Nothing special, just basic math.

After a path is generated you may either use shape-layer approach or draw-rect approach. I will use the draw-rect:

You may subclass an UIView and override a method func draw(_ rect: CGRect). This method will be called whenever a view needs a display and you should NEVER call this method directly. So in order to redraw the view you simply call setNeedsDisplay on the view. Starting with code:

class GradientProgressView: UIView {

var points: [CGPoint]? { didSet { setNeedsDisplay() } }

override func draw(_ rect: CGRect) {
super.draw(rect)

guard let context = UIGraphicsGetCurrentContext() else { return }

let lineWidth: CGFloat = 5.0
guard let points = points else { return }
guard let path = generatePath(withPoints: points, inFrame: bounds.insetBy(dx: lineWidth, dy: lineWidth)) else { return }

drawGradient(path: path, context: context)
drawLine(path: path, lineWidth: lineWidth, context: context)
}

Nothing very special. The context is grabbed for drawing the gradient and for clipping (later). Other than that the path is created using the previous method and then passed to two rendering methods.

Starting with the line things get very simple:

private func drawLine(path: UIBezierPath, lineWidth: CGFloat, context: CGContext) {
UIColor.black.setStroke()
path.lineWidth = lineWidth
path.stroke()
}

there should most likely be a property for color but I just hardcoded it.

As for gradient things do get a bit more scary:

private func drawGradient(path: UIBezierPath, context: CGContext) {
context.saveGState()

path.addClip() // This will be discarded once restoreGState() is called
let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: [UIColor.blue, UIColor.green].map { $0.cgColor } as CFArray, locations: [0.0, 1.0])!
context.drawRadialGradient(gradient, startCenter: CGPoint(x: bounds.midX, y: bounds.midY), startRadius: 0.0, endCenter: CGPoint(x: bounds.midX, y: bounds.midY), endRadius: min(bounds.width, bounds.height), options: [])

context.restoreGState()
}

When drawing a radial gradient you need to clip it with your path. This is done by calling path.addClip() which uses a "fill" approach on your path and applies it to current context. This means that everything you draw after this call will be clipped to this path and outside of it nothing will be drawn. But you DO want to draw outside of it later (the line does) and you need to reset the clip. This is done by saving and restoring state on your current context calling saveGState and restoreGState. These calls are push-pop so for every "save" there should be a "restore". And you can nest this procedure (as it will be done when applying a progress).

Using just this code you should already be able to draw your full shape (as in with 100% progress). To give my test example I use it all in code like this:

class ViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()

let progressView = GradientProgressView(frame: .init(x: 30.0, y: 30.0, width: 280.0, height: 350.0))
progressView.backgroundColor = UIColor.lightGray // Just to debug
progressView.points = {
let count = 200
let minimumRadius: CGFloat = 0.9
let maximumRadius: CGFloat = 1.1

return (0...count).map { index in
let progress: CGFloat = CGFloat(index) / CGFloat(count)
let angle = CGFloat.pi * 2.0 * progress
let radius = CGFloat.random(in: minimumRadius...maximumRadius)
return .init(x: cos(angle)*radius, y: sin(angle)*radius)
}
}()
view.addSubview(progressView)
}


}

Adding a progress now only needs additional clipping. We would like to draw only within a certain angle. This should be straight forward by now:

Another property is added to the view:

var progress: CGFloat = 0.7 { didSet { setNeedsDisplay() } }

I use progress as value between 0 and 1 where 0 is 0% progress and 1 is 100% progress.

Then to create a clipping path:

private func createProgressClippingPath() -> UIBezierPath {
let endAngle = CGFloat.pi*2.0*progress

let maxRadius: CGFloat = max(bounds.width, bounds.height) // we simply need one that is large enough.
let path = UIBezierPath()
let center: CGPoint = .init(x: bounds.midX, y: bounds.midY)
path.move(to: center)
path.addArc(withCenter: center, radius: maxRadius, startAngle: 0.0, endAngle: endAngle, clockwise: true)
return path
}

This is simply a path from center and creating an arc from zero angle to progress angle.

Now to apply this additional clipping:

override func draw(_ rect: CGRect) {
super.draw(rect)

let actualProgress = max(0.0, min(progress, 1.0))
guard actualProgress > 0.0 else { return } // Nothing to draw

let willClipAsProgress = actualProgress < 1.0

guard let context = UIGraphicsGetCurrentContext() else { return }

let lineWidth: CGFloat = 5.0
guard let points = points else { return }
guard let path = generatePath(withPoints: points, inFrame: bounds.insetBy(dx: lineWidth, dy: lineWidth)) else { return }

if willClipAsProgress {
context.saveGState()
createProgressClippingPath().addClip()
}


drawGradient(path: path, context: context)
drawLine(path: path, lineWidth: lineWidth, context: context)

if willClipAsProgress {
context.restoreGState()
}
}

We really just want to apply clipping when progress is not full. And we want to discard all drawing when progress is at zero since everything would be clipped.

You can see that the start angle of the shape is toward right instead of facing upward. Let's apply some transformation to fix that:

progressView.transform = CGAffineTransform(rotationAngle: -.pi*0.5)

At this point the new view is capable of drawing and redrawing itself. You are free to use this in storyboard, you can add inspectables and make it designable if you will. As for the animation you are now only looking to animate a simple float value and assign it to progress. There are many ways to do that and I will do the laziest, which is using a timer:

@objc private func animateProgress() {
let duration: TimeInterval = 1.0
let startDate = Date()

Timer.scheduledTimer(withTimeInterval: 1.0/60.0, repeats: true) { [weak self] timer in
guard let self = self else {
timer.invalidate()
return
}

let progress = Date().timeIntervalSince(startDate)/duration

if progress >= 1.0 {
timer.invalidate()
}
self.progressView?.progress = max(0.0, min(CGFloat(progress), 1.0))
}
}

This is pretty much it. A full code that I used to play around with this:

class ViewController: UIViewController {

private var progressView: GradientProgressView?

override func viewDidLoad() {
super.viewDidLoad()

let progressView = GradientProgressView(frame: .init(x: 30.0, y: 30.0, width: 280.0, height: 350.0))
progressView.backgroundColor = UIColor.lightGray // Just to debug
progressView.transform = CGAffineTransform(rotationAngle: -.pi*0.5)
progressView.points = {
let count = 200
let minimumRadius: CGFloat = 0.9
let maximumRadius: CGFloat = 1.1

return (0...count).map { index in
let progress: CGFloat = CGFloat(index) / CGFloat(count)
let angle = CGFloat.pi * 2.0 * progress
let radius = CGFloat.random(in: minimumRadius...maximumRadius)
return .init(x: cos(angle)*radius, y: sin(angle)*radius)
}
}()
view.addSubview(progressView)
self.progressView = progressView

view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(animateProgress)))
}

@objc private func animateProgress() {
let duration: TimeInterval = 1.0
let startDate = Date()

Timer.scheduledTimer(withTimeInterval: 1.0/60.0, repeats: true) { [weak self] timer in
guard let self = self else {
timer.invalidate()
return
}

let progress = Date().timeIntervalSince(startDate)/duration

if progress >= 1.0 {
timer.invalidate()
}
self.progressView?.progress = max(0.0, min(CGFloat(progress), 1.0))
}
}


}

private extension ViewController {

class GradientProgressView: UIView {

var points: [CGPoint]? { didSet { setNeedsDisplay() } }
var progress: CGFloat = 0.7 { didSet { setNeedsDisplay() } }

override func draw(_ rect: CGRect) {
super.draw(rect)

let actualProgress = max(0.0, min(progress, 1.0))
guard actualProgress > 0.0 else { return } // Nothing to draw

let willClipAsProgress = actualProgress < 1.0

guard let context = UIGraphicsGetCurrentContext() else { return }

let lineWidth: CGFloat = 5.0
guard let points = points else { return }
guard let path = generatePath(withPoints: points, inFrame: bounds.insetBy(dx: lineWidth, dy: lineWidth)) else { return }

if willClipAsProgress {
context.saveGState()
createProgressClippingPath().addClip()
}


drawGradient(path: path, context: context)
drawLine(path: path, lineWidth: lineWidth, context: context)

if willClipAsProgress {
context.restoreGState()
}
}

private func createProgressClippingPath() -> UIBezierPath {
let endAngle = CGFloat.pi*2.0*progress

let maxRadius: CGFloat = max(bounds.width, bounds.height) // we simply need one that is large enough.
let path = UIBezierPath()
let center: CGPoint = .init(x: bounds.midX, y: bounds.midY)
path.move(to: center)
path.addArc(withCenter: center, radius: maxRadius, startAngle: 0.0, endAngle: endAngle, clockwise: true)
return path
}

private func drawGradient(path: UIBezierPath, context: CGContext) {
context.saveGState()

path.addClip() // This will be discarded once restoreGState() is called
let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: [UIColor.blue, UIColor.green].map { $0.cgColor } as CFArray, locations: [0.0, 1.0])!
context.drawRadialGradient(gradient, startCenter: CGPoint(x: bounds.midX, y: bounds.midY), startRadius: 0.0, endCenter: CGPoint(x: bounds.midX, y: bounds.midY), endRadius: min(bounds.width, bounds.height), options: [])

context.restoreGState()
}

private func drawLine(path: UIBezierPath, lineWidth: CGFloat, context: CGContext) {
UIColor.black.setStroke()
path.lineWidth = lineWidth
path.stroke()
}



private func generatePath(withPoints points: [CGPoint], inFrame frame: CGRect) -> UIBezierPath? {
guard points.count > 2 else { return nil } // At least 3 points
let pointsInPolarCoordinates: [(angle: CGFloat, radius: CGFloat)] = points.map { point in
let radius = (point.x*point.x + point.y*point.y).squareRoot()
let angle = atan2(point.y, point.x)
return (angle, radius)
}
let maximumPointRadius: CGFloat = pointsInPolarCoordinates.max(by: { $1.radius > $0.radius })!.radius
guard maximumPointRadius > 0.0 else { return nil } // Not all points may be centered

let maximumFrameRadius = min(frame.width, frame.height)*0.5
let radiusScale = maximumFrameRadius/maximumPointRadius

let normalizedPoints: [CGPoint] = pointsInPolarCoordinates.map { polarPoint in
.init(x: frame.midX + cos(polarPoint.angle)*polarPoint.radius*radiusScale,
y: frame.midY + sin(polarPoint.angle)*polarPoint.radius*radiusScale)
}

let path = UIBezierPath()
path.move(to: normalizedPoints[0])
normalizedPoints[1...].forEach { path.addLine(to: $0) }
path.close()
return path
}

}

}


Related Topics



Leave a reply



Submit