I have a simple custom CALayer to create an overlaying gradient effect on my UIView. Here is the code:
class GradientLayer: CALayer {
var locations: [CGFloat]?
var origin: CGPoint?
var radius: CGFloat?
var color: CGColor?
convenience init(view: UIView, locations: [CGFloat]?, origin: CGPoint?, radius: CGFloat?, color: UIColor?) {
self.init()
self.locations = locations
self.origin = origin
self.radius = radius
self.color = color?.CGColor
self.frame = view.bounds
}
override func drawInContext(ctx: CGContext) {
super.drawInContext(ctx)
guard let locations = self.locations else { return }
guard let origin = self.origin else { return }
guard let radius = self.radius else { return }
let colorSpace = CGColorGetColorSpace(color)
let colorComponents = CGColorGetComponents(color)
let gradient = CGGradientCreateWithColorComponents(colorSpace, colorComponents, locations, locations.count)
CGContextDrawRadialGradient(ctx, gradient, origin, CGFloat(0), origin, radius, [.DrawsAfterEndLocation])
}
}
I initialize and set these layers here:
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
let gradient1 = GradientLayer(view: view, locations: [0.0,1.0], origin: CGPoint(x: view.frame.midX, y: view.frame.midY), radius: 100.0, color: UIColor(white: 1.0, alpha: 0.2))
let gradient2 = GradientLayer(view: view, locations: [0.0,1.0], origin: CGPoint(x: view.frame.midX-20, y: view.frame.midY+20), radius: 160.0, color: UIColor(white: 1.0, alpha: 0.2))
let gradient3 = GradientLayer(view: view, locations: [0.0,1.0], origin: CGPoint(x: view.frame.midX+30, y: view.frame.midY-30), radius: 300.0, color: UIColor(white: 1.0, alpha: 0.2))
gradient1.setNeedsDisplay()
gradient2.setNeedsDisplay()
gradient3.setNeedsDisplay()
view.layer.addSublayer(gradient1)
view.layer.addSublayer(gradient2)
view.layer.addSublayer(gradient3)
}
The view seems to display properly most of the time, but (seemingly) randomly I'll get different renderings as you'll see below. Here are some examples (the first one is what I want):
What is causing this malfunction? How do I only load the first one every time?
Your problem is the code you have written in viewWillLayoutSubviews function its is called multiple times when views loads just add a check to run it once or better yet add a check in viewdidlayoutsubviews to run it once
You have several problems.
First off, you should think of a gradient as an array of stops, where a stop has two parts: a color and a location. You must have an equal number of colors and locations, because every stop has one of each. You can see this if, for example, you check the
CGGradientCreateWithColorComponents
documentation regarding thecomponents
argument:It's a product (the result of a multiplication) because you have
count
stops and you need a complete set of color components for each stop.You're not providing enough color components. Your
GradientLayer
could have any number of locations (and you're giving it two) but has only one color. You're getting that one color's components and passing that as the components array toCGGradientCreateWithColorComponents
, but the array is too short. Swift doesn't catch this error—notice that the type of yourcolorComponents
isUnsafePointer<CGFloat>
. TheUnsafe
part tells you that you're in dangerous territory. (You can see the type ofcolorComponents
by option-clicking it in Xcode.)Since you're not providing a large enough array for
components
, iOS is using whatever random values happen to be in memory after the components of your one color. Those may change from run to run and are often not what you want them to be.In fact, you shouldn't even use
CGGradientCreateWithColorComponents
. You should useCGGradientCreateWithColors
, which takes an array ofCGColor
so it's not only simpler to use, but safer because it's one lessUnsafePointer
floating around.Here's what
GradientLayer
should look like:Next problem. You're adding more gradient layers every time the system calls
viewWillLayoutSubviews
. It can call that function multiple times! For example, it will call it if your app supports interface rotation, or if a call comes in and iOS makes the status bar taller. (You can test that in the simulator by choosing Hardware > Toggle In-Call Status Bar.)You need to create the gradient layers once, storing them in a property. If they have already been created, you need to update their frames and not create new layers: