UIView level sublayers appear differently / randomly each time a view is displayed

I have a simple custom CALayer to create an overlay 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 the following 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) } 

It seems that the view is displayed correctly in most cases, but (apparently) randomly, I get different visualizations, as you will see below. Here are a few examples (the first is what I want):

enter image description here enter image description here enter image description here

What causes this malfunction? How to download the first time every time?

+1
ios swift calayer uiview cagradientlayer
May 6 '16 at 10:36
source share
2 answers

You have a few problems.

First, you should think of the gradient as a lot of stops, where the stop consists of two parts: color and location. You should have the same number of colors and places, because each stop has one. You can see this if, for example, you check the CGGradientCreateWithColorComponents documentation for the components argument:

The number of elements in this array should be the product of count and the number of components in the color space.

This is a product (the result of multiplication), because your count stops, and you need a full set of color components for each stop.

You do not provide enough color components. Your GradientLayer can have any number of places (and you give it two), but it has only one color. You get this one color component and pass it as an array of components in the CGGradientCreateWithColorComponents , but the array is too short. Swift will not catch this error - note that the type of your colorComponents is UnsafePointer<CGFloat> . The Unsafe part tells you that you are in a dangerous area. (You can see the colorComponents type by clicking on the icon in Xcode.)

Since you are not providing a sufficiently large array for components , iOS uses any random values ​​that are in memory after the components of your same color. They can change from run to run and are often not what you want.

In fact, you should not even use CGGradientCreateWithColorComponents . You should use CGGradientCreateWithColors , which accepts a CGColor array, so it is not only easier to use, but also safer because it floats around less than UnsafePointer .

Here GradientLayer should look like this:

 class RadialGradientLayer: CALayer { struct Stop { var location: CGFloat var color: UIColor } var stops: [Stop] { didSet { self.setNeedsDisplay() } } var origin: CGPoint { didSet { self.setNeedsDisplay() } } var radius: CGFloat { didSet { self.setNeedsDisplay() } } init(stops: [Stop], origin: CGPoint, radius: CGFloat) { self.stops = stops self.origin = origin self.radius = radius super.init() needsDisplayOnBoundsChange = true } override init(layer other: AnyObject) { guard let other = other as? RadialGradientLayer else { fatalError() } stops = other.stops origin = other.origin radius = other.radius super.init(layer: other) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func drawInContext(ctx: CGContext) { let locations = stops.map { $0.location } let colors = stops.map { $0.color.CGColor } locations.withUnsafeBufferPointer { pointer in let gradient = CGGradientCreateWithColors(nil, colors, pointer.baseAddress) CGContextDrawRadialGradient(ctx, gradient, origin, 0, origin, radius, [.DrawsAfterEndLocation]) } } } 

The next problem. You add more gradient layers each time the system calls viewWillLayoutSubviews . It can call this function several times! For example, it will call it if your application supports rotating the interface, or if a call comes in, and iOS makes the status bar taller. (You can check this in the simulator by choosing Equipment> Switch Status Bar During A Call.)

You need to create gradient layers once, saving them in the property. If they are already created, you need to update their frames and not create new layers:

 class ViewController: UIViewController { private var gradientLayers = [RadialGradientLayer]() override func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() if gradientLayers.isEmpty { createGradientLayers() } for layer in gradientLayers { layer.frame = view.bounds } } private func createGradientLayers() { let bounds = view.bounds let mid = CGPointMake(bounds.midX, bounds.midY) typealias Stop = RadialGradientLayer.Stop for (point, radius, color) in [ (mid, 100, UIColor(white:1, alpha:0.2)), (CGPointMake(mid.x - 20, mid.y + 20), 160, UIColor(white:1, alpha:0.2)), (CGPointMake(mid.x + 30, mid.y - 30), 300, UIColor(white:1, alpha:0.2)) ] as [(CGPoint, CGFloat, UIColor)] { let stops: [RadialGradientLayer.Stop] = [ Stop(location: 0, color: color), Stop(location: 1, color: color.colorWithAlphaComponent(0))] let layer = RadialGradientLayer(stops: stops, origin: point, radius: radius) view.layer.addSublayer(layer) gradientLayers.append(layer) } } } 
+1
May 7 '16 at 3:54
source share

Your problem is the code that you wrote in the viewWillLayoutSubviews function, it is called several times when the views load, just add a check to run it once or better, but add a check in viewdidlayoutsubviews to run it once

0
May 6 '16 at 23:24
source share



All Articles