Change Gradient Background of UIView Through Sublayer - ios

I've got this function defined in an extension of UIView which I use to set the gradient background on a UIView:
func setGradientBackground(colorTop: UIColor, colorBottom: UIColor, withCornerRadius : CGFloat){
let gradientLayer = CAGradientLayer()
gradientLayer.colors = [colorBottom.cgColor, colorTop.cgColor]
gradientLayer.startPoint = CGPoint(x: 0.5, y: 1.0)
gradientLayer.endPoint = CGPoint(x: 0.5, y: 0.0)
gradientLayer.locations = [0, 1]
gradientLayer.frame = bounds
gradientLayer.cornerRadius = withCornerRadius
layer.insertSublayer(gradientLayer, at: 0)
}
It is used as follows:
view.setGradientBackground(colorTop: UIColor.blue, colorBottom: UIColor.red, withCornerRadius: 10)
The problem is, if I have already called this function once on a view, the second time I call it, it does not do anything. I suspect this is caused by the subLayer persisting from the previous insertion.
I've tried adding:
if layer.sublayers?.count ?? 0 > 0 {
layer.sublayers?.remove(at: 0)
}
to the top of that function, but that causes by code to crash with a BAD_INSTRUCTION exception.

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")
}

The easiest option would be to check to see if the layer's sublayer is a gradient layer. This isn't perfectly safe because if you happen to have another gradient layer at index 0 of the sublayers, it will change the incorrect sublayer.
func setGradientBackground(colorTop: UIColor, colorBottom: UIColor, withCornerRadius cornerRadius: CGFloat) {
let gradientLayer = layer.sublayers?.first as? CAGradientLayer ?? CAGradientLayer()
// The rest of your existing layer customization code here
guard gradientLayer.superlayer != self else {
return
}
layer.sublayers?.insertSublayer(gradientLayer, at: 0)
}

Related

UIView with gradient not changing colors when button is tapped

When a user clicks on a button, it randomizes the colors used to create a gradient in a large circle. Beneath it are two small circles that display the solid colors used for the gradient. They all display correctly at the start (main circle is a gradient of the randomized smaller circles' color) but when I click on the button, only the smaller circles change color; the large circle stays at the same gradient colors.
Extensions and View Controller:
extension UIView {
func setupGradientBackground(colorOne: UIColor, colorTwo: UIColor) {
let gradientLayer: CAGradientLayer = CAGradientLayer()
gradientLayer.colors = [colorOne.cgColor, colorTwo.cgColor]
gradientLayer.locations = [0.0, 1.0]
gradientLayer.startPoint = CGPoint(x: 0.0, y: 1.0)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.0)
gradientLayer.frame = self.bounds
self.layer.insertSublayer(gradientLayer, at: 0)
}
}
extension UIColor {
static var random: UIColor {
return UIColor(red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1), alpha: 1.0)
}
}
class GradientController: UIViewController {
let gradientView = GradientView()
let leftGradientColor: UIColor = .random
let rightGradientColor: UIColor = .random
}
override func viewDidLoad() {
super.viewDidLoad()
view = gradientView
newGradient()
}
func newGradient() {
gradientView.mainCircleView.setupGradientBackground(colorOne: leftGradientColor, colorTwo: rightGradientColor)
gradientView.colorCircleLeftView.backgroundColor = leftGradientColor
gradientView.colorCircleRightView.backgroundColor = rightGradientColor
gradientView.gradientGenerateButton.addTarget(self, action: #selector(randomGradient(sender:)), for: .touchUpInside)
}
#objc func randomGradient(sender: UIButton)
{
let leftGradient = UIColor.random
let rightGradient = UIColor.random
gradientView.colorCircleLeftView.backgroundColor = leftGradient
gradientView.colorCircleRightView.backgroundColor = rightGradient
//Here is where it's not changing colors. Doesn't seem like the VC recognizes it in this function
gradientView.mainCircleView.setupGradientBackground(colorOne: leftGradient, colorTwo: rightGradient)
}
View:
class GradientView: UIView {
//circle's UIView code in Extensions
let mainCircleView = UIView().circleView(width: 380, height: 380)
let colorCircleLeftView = UIView().circleView(width: 40, height: 40)
let colorCircleRightView = UIView().circleView(width: 40, height: 40)
...
func setupLayout() {
...
}
}
What I've tried is changing the mainCircleView color to solid UIColors, like gradientView.mainCircleView.setupGradientBackground(colorOne: .red, colorTwo: .orange) to see if the main Circle changes to those colors in both func newGradient() and #objc func randomGradient(sender: UIButton). It only changes in func newGradient() what I've set manually, so that means the VC isn't recognizing the main Circle in the #objc func but I'm lost on how to fix it...
Any help is appreciated!
What it looks like when I click the "Generate" button (large circle should be showing brown and purple):
Update your function with this, You have to remove old layer and then insert new Sublayer.
Solution 1 :
func setupGradientBackground(colorOne: UIColor, colorTwo: UIColor) {
if let gradientLayer = (self.layer.sublayers?.compactMap { $0 as? CAGradientLayer })?.first {
gradientLayer.removeFromSuperlayer()
}
let gradientLayer: CAGradientLayer = CAGradientLayer()
gradientLayer.colors = [colorOne.cgColor, colorTwo.cgColor]
gradientLayer.locations = [0.0, 1.0]
gradientLayer.startPoint = CGPoint(x: 0.0, y: 1.0)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.0)
gradientLayer.frame = self.bounds
self.layer.insertSublayer(gradientLayer, at: 0)
}
Solution 2 :
func setupGradientBackground(colorOne: UIColor, colorTwo: UIColor) {
let gradientLayer = layer.sublayers?.first as? CAGradientLayer ?? CAGradientLayer()
gradientLayer.colors = [colorOne.cgColor, colorTwo.cgColor]
gradientLayer.locations = [0.0, 1.0]
gradientLayer.startPoint = CGPoint(x: 0.0, y: 1.0)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.0)
gradientLayer.frame = self.bounds
guard gradientLayer.superlayer != self else {
return
}
layer.insertSublayer(gradientLayer, at: 0)
}
Solution 3 :
you can set name to your CAGradientLayer, This will help you for removing that particular layer.
func setupGradientBackground(colorOne: UIColor, colorTwo: UIColor) {
for layer in layer.sublayers ?? [] {
if layer.name == "GradientLayer" {
layer.removeFromSuperlayer()
}
}
let gradientLayer: CAGradientLayer = CAGradientLayer()
gradientLayer.colors = [colorOne.cgColor, colorTwo.cgColor]
gradientLayer.locations = [0.0, 1.0]
gradientLayer.startPoint = CGPoint(x: 0.0, y: 1.0)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.0)
gradientLayer.name = "GradientLayer"
gradientLayer.frame = self.bounds
self.layer.insertSublayer(gradientLayer, at: 0)
}
The gradient does not change because you add a new gradient below the existing one
extension UIView {
func setupGradientBackground(colorOne: UIColor, colorTwo: UIColor) {
let gradientLayer: CAGradientLayer = CAGradientLayer()
...
// ⬇ There is a mistake.
self.layer.insertSublayer(gradientLayer, at: 0)
}
}
You should change the current gradient with this new one. Or insert a new one above the old one.
Changing the gradient layer colors after setting it
extension CALayer {
func updateGradientColors(_ colors:CGColor...) {
if let layer = sublayers?.first as? CAGradientLayer {
let frame = CGRect(origin: self.bounds.origin, size: CGSize(width: self.bounds.size.width, height: self.bounds.size.height))
layer.frame = frame
layer.colors = colors
}
}
}

iOS Gradient color isse in collection view cell

I am facing this issue while setting gradient color to view inside the collection view
When its plotted at first time its fine.
but when I select some option and reload collection for the same it's happening like the second image.
viccalexander/Chameleon (https://github.com/viccalexander/Chameleon)
Try below code, might help you
extension UIView{
func addGradientBackground(firstColor: UIColor, secondColor: UIColor){
clipsToBounds = true
let gradientLayer = CAGradientLayer()
gradientLayer.colors = [firstColor.cgColor, secondColor.cgColor]
gradientLayer.frame = self.bounds
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
gradientLayer.endPoint = CGPoint(x: 0, y: 1)
print(gradientLayer.frame)
self.layer.insertSublayer(gradientLayer, at: 0)
}
}
And inside your cell simply:
override func awakeFromNib() {
super.awakeFromNib()
DispatchQueue.main.async {
self.addGradientBackground(firstColor: .green, secondColor: .blue)
}
}
Add this line only in Xib file otherwise when cell be reload then is add multiple Gradient in cell self.addGradientBackground(firstColor: .green, secondColor: .blue)
Reference - How to make cells have a gradient background?

horizontal gradient doesn't work on dynamic width content

I am trying to implement gradient for button or view depending on requirement. But whenever I try to do so for button/view who's width is dynamic (for horizontal gradient) it only loads half the same.
func applyGradient(withColours colours: [UIColor], gradientOrientation orientation: GradientOrientation) {
let gradient: CAGradientLayer = CAGradientLayer()
gradient.frame = self.bounds
gradient.colors = colours.map { $0.cgColor }
gradient.startPoint = orientation.startPoint
gradient.endPoint = orientation.endPoint
self.layer.insertSublayer(gradient, at: 0)
}
GradientOrientation is obtained by following code.
enum GradientOrientation {
case topRightBottomLeft
case topLeftBottomRight
case horizontal
case vertical
var startPoint: CGPoint {
return points.startPoint
}
var endPoint: CGPoint {
return points.endPoint
}
var points: GradientPoints {
switch self {
case .topRightBottomLeft:
return (CGPoint.init(x: 0.0, y: 1.0), CGPoint.init(x: 1.0, y: 0.0))
case .topLeftBottomRight:
return (CGPoint.init(x: 0.0, y: 0.0), CGPoint.init(x: 1, y: 1))
case .horizontal:
return (CGPoint.init(x: 0.0, y: 0.5), CGPoint.init(x: 1.0, y: 0.5))
case .vertical:
return (CGPoint.init(x: 0.0, y: 0.0), CGPoint.init(x: 0.0, y: 1.0))
}
}
}
But if the width is 150 it loads up perfectly for both view/button. Any help is appreciated.
Most likely the frame of your button is not the same when you apply the gradient as it is later on. You can try printing bounds in applyGradient to check.
You should add a bit more logic anyway; since this looks like a subclass of a view it would be great to store the layer. Currently what do you expect will happen if applyGradient is called twice? You will most likely have 2 gradients.
So you would add:
private var currentGradientLayer: CAGradientLayer?
func applyGradient(withColours colours: [UIColor], gradientOrientation orientation: GradientOrientation) {
currentGradientLayer?.removeFromSuperlayer()
currentGradientLayer = nil
let gradient: CAGradientLayer = {
let gradient: CAGradientLayer = CAGradientLayer()
gradient.frame = self.bounds
gradient.colors = colours.map { $0.cgColor }
gradient.startPoint = orientation.startPoint
gradient.endPoint = orientation.endPoint
}()
self.layer.insertSublayer(gradient, at: 0)
currentGradientLayer = gradient
}
Now since that is done you should try to override 2 more methods to fix the frames:
override func layoutSubviews() {
super.layoutSubviews()
refreshGradientLayer()
}
override var frame: CGRect {
didSet {
refreshGradientLayer()
}
}
private func refreshGradientLayer() {
currentGradientLayer?.frame = bounds
}
This should refresh the gradient in any case; layoutSubviews will be called when your view is layout with constraints and frame when it is done programmatically.
All sublayers in UIView.layer do not change their size automatically when changing the super layer. Override the method of UIView.layoutSubviews() and update bounds CAGradientLayer from UIView.layer.
<gradientlayer>.layoutIfNeeded()
Call this method after you done calculating the width of the dynamic component.

Swift PageViewController gradient background

I have a PageViewController and need to set a gradient background to it.
When using gradient background in normal ViewControllers, I use the following:
let layer = self.layer as! CAGradientLayer
layer.colors = [FirstColor.cgColor, SecondColor.cgColor]
layer.locations = [0.5]
when I try to use self.view.layer inside my PageViewController I get an error:
Could not cast value of type 'CALayer' (0x104f57900) to 'CAGradientLayer'
Can someone help me setting the background of a page view controller to an gradient?
Thanks
Hi you can create a UIView extension and just call like this:
view.applyGradient(colours: [UIColor.red, UIColor.green])
here the extension code
extension UIView {
func applyGradient(colours: [UIColor]) -> Void {
clipsToBounds = true
let gradient: CAGradientLayer = CAGradientLayer()
gradient.frame = self.bounds
gradient.colors = colours.map { $0.cgColor }
gradient.startPoint = CGPoint(x: 0, y: 0)
gradient.endPoint = CGPoint(x: 1, y: 0)
layer.insertSublayer(gradient, at: 0)
}
}
with this code you can apply to UIButtons, UILabels, anything that extends from UIView
hope it helps

iOS UIProgressView with gradient

is it possible to create a custom ui progress view with a gradient from left to right?
I've tried it with the following code:
let gradientLayer = CAGradientLayer()
gradientLayer.frame = self.frame
gradientLayer.anchorPoint = CGPoint(x: 0, y: 0)
gradientLayer.position = CGPoint(x: 0, y: 0)
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0);
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.0);
gradientLayer.colors = [
UIColor.red,
UIColor.green
]
// Convert to UIImage
self.layer.insertSublayer(gradientLayer, at: 0)
self.progressTintColor = UIColor.clear
self.trackTintColor = UIColor.black
But unfortunately the gradient is not visible. Any other ideas?
Looking at UIProgressView documentation, there's this property:
progressImage
If you provide a custom image, the progressTintColor property is ignored.
With that in mind, the laziest way to do this would be to create your gradient image and set it as the progressImage
I adapted this extension to make it a little cleaner, scaleable, and safer.
fileprivate extension UIImage {
static func gradientImage(with bounds: CGRect,
colors: [CGColor],
locations: [NSNumber]?) -> UIImage? {
let gradientLayer = CAGradientLayer()
gradientLayer.frame = bounds
gradientLayer.colors = colors
// This makes it horizontal
gradientLayer.startPoint = CGPoint(x: 0.0,
y: 0.5)
gradientLayer.endPoint = CGPoint(x: 1.0,
y: 0.5)
UIGraphicsBeginImageContext(gradientLayer.bounds.size)
gradientLayer.render(in: UIGraphicsGetCurrentContext()!)
guard let image = UIGraphicsGetImageFromCurrentImageContext() else { return nil }
UIGraphicsEndImageContext()
return image`
}
}
Now that we've got a way to create a gradient image "on the fly", here's how to use it:
let gradientImage = UIImage.gradientImage(with: progressView.frame,
colors: [UIColor.red.cgColor, UIColor.green.cgColor],
locations: nil)
From there, you'd just set your progressView's progressImage, like so:
// I'm lazy...don't force unwrap this
progressView.progressImage = gradientImage!
progressView.setProgress(0.75, animated: true)
I had the same problem and solved it by creating a gradient custom view which I then convert to an image and assign it as the progress view track image.
I then flip the progress horizontally so that the progress bar becomes the background and the track image becomes the foreground.
This has the visual effect of revealing the gradient image underneath.
You just have to remember to invert your percentages which is really simple, see example buttons and code below:
SWIFT 3 Example:
class ViewController: UIViewController {
#IBOutlet weak var progressView: UIProgressView!
#IBAction func lessButton(_ sender: UIButton) {
let percentage = 20
let invertedValue = Float(100 - percentage) / 100
progressView.setProgress(invertedValue, animated: true)
}
#IBAction func moreButton(_ sender: UIButton) {
let percentage = 80
let invertedValue = Float(100 - percentage) / 100
progressView.setProgress(invertedValue, animated: true)
}
override func viewDidLoad() {
super.viewDidLoad()
//create gradient view the size of the progress view
let gradientView = GradientView(frame: progressView.bounds)
//convert gradient view to image , flip horizontally and assign as the track image
progressView.trackImage = UIImage(view: gradientView).withHorizontallyFlippedOrientation()
//invert the progress view
progressView.transform = CGAffineTransform(scaleX: -1.0, y: -1.0)
progressView.progressTintColor = UIColor.black
progressView.progress = 1
}
}
extension UIImage{
convenience init(view: UIView) {
UIGraphicsBeginImageContext(view.frame.size)
view.layer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
self.init(cgImage: (image?.cgImage)!)
}
}
#IBDesignable
class GradientView: UIView {
private var gradientLayer = CAGradientLayer()
private var vertical: Bool = false
override func draw(_ rect: CGRect) {
super.draw(rect)
// Drawing code
//fill view with gradient layer
gradientLayer.frame = self.bounds
//style and insert layer if not already inserted
if gradientLayer.superlayer == nil {
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
gradientLayer.endPoint = vertical ? CGPoint(x: 0, y: 1) : CGPoint(x: 1, y: 0)
gradientLayer.colors = [UIColor.green.cgColor, UIColor.red.cgColor]
gradientLayer.locations = [0.0, 1.0]
self.layer.insertSublayer(gradientLayer, at: 0)
}
}
}
George figured out a very clever method. If you want a more easy solution, open UIProgressView document, there is a property named progressImage.
so, i just make it work like this:
progressView.progressImage = UIImage(named: "your_gradient_progress_icon")
progressView.trackTintColor = UIColor.clear
after that:
progressView.setProgress(currentProgress, animated: true)

Resources