Applying gradient layer over a UIBezierPath graph - swift

This is my first time asking a question, so pardon me if it not very thorough.
Basically I am trying to apply a gradient layer on top of a UIBezierPath that is drawn as a graph.
Here is my graph:
Here is the gradient layer I am going for:
Here is what I have tried to draw my graph:
let path = quadCurvedPathWithPoints(points: points)
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.strokeColor = UIColor.blue.cgColor
shapeLayer.fillColor = UIColor.white.cgColor
shapeLayer.position = CGPoint(x: 0, y: 0)
shapeLayer.lineWidth = 1.0
This fills the graph with a blue stroke, obviously, but now I am trying to apply the white to green gradient. The way I am doing it is not working. Here is the result:
Here is the code I am struggling with:
let startColor = UIColor.white.withAlphaComponent(0.5)
let endColor = UIColor.green
let gradient = CAGradientLayer()
gradient.colors = [startColor.cgColor, endColor.cgColor]
gradient.startPoint = CGPoint(x: 0.0, y: 0.5)
gradient.endPoint = CGPoint(x: 1.0, y: 0.5)
gradient.frame = path.bounds
let shapeMask = CAShapeLayer()
shapeMask.path = path.cgPath
gradient.mask = shapeLayer
sparkLineView.layer.addSublayer(gradient)

Here's a little demo that draws a UIBezierPath path using a gradient. You can copy this into a Swift playground.
class GradientGraph: UIView {
override func draw(_ rect: CGRect) {
// Create the "graph"
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: frame.height * 0.5))
path.addLine(to: CGPoint(x: frame.width * 0.2, y: frame.height * 0.3))
path.addLine(to: CGPoint(x: frame.width * 0.4, y: frame.height * 0.8))
path.addLine(to: CGPoint(x: frame.width * 0.6, y: frame.height * 0.4))
path.addLine(to: CGPoint(x: frame.width * 0.8, y: frame.height * 0.7))
// Create the gradient
let gradient = CGGradient(colorsSpace: nil, colors: [UIColor.white.cgColor,UIColor.green.cgColor] as CFArray, locations: nil)!
// Draw the graph and apply the gradient
let ctx = UIGraphicsGetCurrentContext()!
ctx.setLineWidth(6)
ctx.saveGState()
ctx.addPath(path.cgPath)
ctx.replacePathWithStrokedPath()
ctx.clip()
ctx.drawLinearGradient(gradient, start: CGPoint(x: 0, y: 0), end: CGPoint(x: frame.width, y: 0), options: [])
ctx.restoreGState()
}
}
let graph = GradientGraph(frame: CGRect(x: 0, y: 0, width: 700, height: 700))

Related

How to blur the border of the mask for an NSImage?

I am trying to blur the border of the mask for an NSImage in swift and I can't figure out, how to do this. The mask is supposed to be created from a CGMutablePath, so that it can be changed programmatically. This is what it looks like at the moment:
This is what it is supposed to look like:
The code for creating the mask with sharp edges is the following:
class MyView: NSView {
override func draw(_ dirtyRect: NSRect) {
let bottomLayer = CALayer()
bottomLayer.bounds = self.bounds
bottomLayer.position = CGPoint(x: self.bounds.size.width/2, y: self.bounds.size.height/2)
bottomLayer.contents = NSImage(named: "bottom.jpg")
bottomLayer.contentsGravity = .resize
self.layer?.addSublayer(bottomLayer)
let path = CGMutablePath()
path.move(to: CGPoint(x: self.bounds.size.width/2, y: self.bounds.size.height/2))
path.addCurve(to: CGPoint(x: 0.5*self.bounds.size.width, y: self.bounds.size.height), control1: CGPoint(x: 0.4*self.bounds.size.width, y: 0), control2: CGPoint(x: 0, y: self.bounds.size.height))
path.addCurve(to: CGPoint(x: self.bounds.size.width/2, y: self.bounds.size.height/2), control1: CGPoint(x: self.bounds.size.width, y: self.bounds.size.height), control2: CGPoint(x: 0.8*self.bounds.size.width, y: 0))
path.closeSubpath()
let maskLayer = CAShapeLayer()
maskLayer.path = path
maskLayer.fillRule = .evenOdd
maskLayer.borderWidth = 20
let topLayer = CALayer()
topLayer.bounds = self.bounds
topLayer.position = CGPoint(x: self.bounds.size.width/2, y: self.bounds.size.height/2)
topLayer.contents = NSImage(named: "top.jpg")
topLayer.contentsGravity = .resize
topLayer.masksToBounds = true
topLayer.mask = maskLayer
self.layer?.addSublayer(topLayer)
}
}
I hope there is a nice solution to this :)
Frederik
Try adding CAGradientLayer instead of CAShapeLayer:
Sample Source:
class MyView: NSView {
override func draw(_ dirtyRect: NSRect) {
let bottomLayer = CALayer()
bottomLayer.bounds = self.bounds
bottomLayer.position = CGPoint(x: self.bounds.size.width/2, y: self.bounds.size.height/2)
bottomLayer.contents = NSImage(named: "bottom")
bottomLayer.contentsGravity = .resize
self.layer?.addSublayer(bottomLayer)
let path = CGMutablePath()
path.move(to: CGPoint(x: self.bounds.size.width/2, y: self.bounds.size.height/2))
path.addCurve(to: CGPoint(x: 0.5*self.bounds.size.width, y: self.bounds.size.height), control1: CGPoint(x: 0.4*self.bounds.size.width, y: 0), control2: CGPoint(x: 0, y: self.bounds.size.height))
path.addCurve(to: CGPoint(x: self.bounds.size.width/2, y: self.bounds.size.height/2), control1: CGPoint(x: self.bounds.size.width, y: self.bounds.size.height), control2: CGPoint(x: 0.8*self.bounds.size.width, y: 0))
path.closeSubpath()
let maskLayer = CAGradientLayer()
maskLayer.shadowRadius = 4
maskLayer.shadowPath = path
maskLayer.shadowOpacity = 1
maskLayer.shadowOffset = CGSize.zero
maskLayer.shadowColor = NSColor.green.cgColor
let topLayer = CALayer()
topLayer.bounds = self.bounds
topLayer.position = CGPoint(x: self.bounds.size.width/2, y: self.bounds.size.height/2)
topLayer.contents = NSImage(named: "top")
topLayer.contentsGravity = .resize
topLayer.masksToBounds = true
topLayer.mask = maskLayer
self.layer?.addSublayer(topLayer)
}
}
Output

How to make a CAshapeLayer fit into a button?

I tried to create a function inside a custom UIButton class to add a shape to an existing button.
func drawStartButton(){
let shape = CAShapeLayer()
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: 500, y: 0))
path.addLine(to: CGPoint(x: 500, y: -100))
path.addLine(to: CGPoint(x: 250, y: -50))
path.addLine(to: CGPoint(x: 0, y: -100))
path.addLine(to: CGPoint(x: 0, y: 0))
path.close()
shape.path = path.cgPath
shape.fillColor = redColor.cgColor
shape.frame = self.bounds
self.layer.addSublayer(shape)
}
So far no problem... BUT when i add the layer to the button, the layer is to big of course! How can i "autoresize" the layer to its button? I did expect something like
shape.frame = self.bounds
... but the path still keeps the same size as without the self.bounds.
Your best bet is to subclass the button and then layout your layer in layoutSubviews which gets called when your surrounding frame changes.
final class StartButton: UIButton {
private let shapeLayer = CAShapeLayer()
override func layoutSubviews() {
super.layoutSubviews()
shapeLayer.frame = bounds
}
}
But I'm noticing that you are dictating a giant shape in terms of your CGPoints. You might have to subclass the layer, too, and redraw based on your bounds in layoutSublayers.
class CustomShapeCAshapeLayer: CAShapeLayer {
func shapeButton(width: CGFloat, height: CGFloat){
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: width, y: 0))
path.addLine(to: CGPoint(x: width, y: height + 30))
path.addLine(to: CGPoint(x: width/2, y: height))
path.addLine(to: CGPoint(x: 0, y: height + 30))
path.addLine(to: CGPoint(x: 0, y: 0))
path.close()
self.fillColor = redColor.cgColor
self.path = path.cgPath
}
}
main:
let layer = CustomShapeCAshapeLayer()
layer.shapeButton(width: startGameButton.frame.width, height: startGameButton.frame.height)
startGameButton.layer.addSublayer(layer)

How to cut of corners of UIButton, not round them?

I would like to cut the corners off of a UIButton in swift.
I know how to set a corner radius but I'd like to cut the corners.
// what I DON'T want, rounded corners
myButton.layer.cornerRadius = myButton.frame.height / 2
Cut off corners are for me:
Create a CAShapeLayer with the desired path like this
let button = UIButton()
button.backgroundColor = .red
button.frame = CGRect(x: 100, y: 100, width: 200, height: 40)
view.addSubview(button)
let layer = CAShapeLayer()
layer.fillColor = UIColor.yellow.cgColor
let cutSize:CGFloat = 20
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: button.bounds.midY))
path.addLine(to: CGPoint(x: cutSize, y: 0))
path.addLine(to: CGPoint(x: button.bounds.maxX-cutSize, y: 0))
path.addLine(to: CGPoint(x: button.bounds.maxX, y: button.bounds.midY))
path.addLine(to: CGPoint(x: button.bounds.maxX-cutSize, y: button.bounds.maxY))
path.addLine(to: CGPoint(x: cutSize, y: button.bounds.maxY))
path.addLine(to: CGPoint(x: 0, y: button.bounds.midY))
layer.path = path.cgPath
button.layer.insertSublayer(layer, at: 0)
You can achieve by following:
sampleButton.layer.cornerRadius = 15
sampleButton.clipsToBounds = true

Add shadow to a UIView with layer

I'm trying to add shadow to a view with layer and gradient. Following what i was able to realize:
1) The path
let path = UIBezierPath()
path.move(to: CGPoint(x: myView.frame.minX, y: myView.frame.minY))
path.addLine(to: CGPoint(x: myView.frame.minX, y: myView.frame.maxY))
path.addLine(to: CGPoint(x: myView.frame.maxX, y: 171))
path.addLine(to: CGPoint(x: myView.frame.maxX, y: myView.frame.minY))
path.close()
2) The shape
let shape = CAShapeLayer()
shape.path = path.cgPath
3) The gradient and shadow
let gradient = CAGradientLayer()
gradient.frame = myView.bounds
gradient.colors = [UIColor.red.cgColor, UIColor.blue.cgColor]
gradient.startPoint = CGPoint(x: 0, y: 0)
gradient.endPoint = CGPoint(x: 1, y: 1)
let shadow = CALayer()
shadow.shadowPath = path.cgPath
shadow.shadowColor = UIColor.black.cgColor
shadow.shadowOffset = CGSize.zero
shadow.shadowOpacity = 0.5
4) Put all layers as sublayer of my view
self.myView.layer.addSublayer(shape)
self.myView.layer.addSublayer(gradient)
self.myView.layer.addSublayer(shadow)
self.myView.layer.mask = shape
here what simulator shows to me: what?!?! the shadows is placed on the view, not below!!!
What could I do to fix it? my final purpose is to put the shadow below the view (as the red line).
3) Set the shape as a mask to the gradient. Set shadow to myView.layer
let gradient = CAGradientLayer()
gradient.frame = myView.bounds
gradient.colors = [UIColor.red.cgColor, UIColor.blue.cgColor]
gradient.startPoint = CGPoint(x: 0, y: 0)
gradient.endPoint = CGPoint(x: 1, y: 1)
gradient.mask = shape // <-- 1
let shadow = myView.layer // <-- 2
shadow.shadowPath = path.cgPath
shadow.shadowColor = UIColor.black.cgColor
shadow.shadowOffset = CGSize.zero
shadow.shadowOpacity = 0.5
4) Just put the gradient as a sublayer of myView.layer
self.myView.layer.addSublayer(gradient) // <-- 3
You should assign the shadow radius as well.
shadow.shadowRadius = 5

Swift draw shadow to a uibezier path

I have a strange question. Even though I did read a lot of tutorials on how to do this, the final result only shows the bezier line, not any shadow whatsoever. My code is pretty simple :
let borderLine = UIBezierPath()
borderLine.moveToPoint(CGPoint(x:0, y: y! - 1))
borderLine.addLineToPoint(CGPoint(x: x!, y: y! - 1))
borderLine.lineWidth = 2
UIColor.blackColor().setStroke()
borderLine.stroke()
let shadowLayer = CAShapeLayer()
shadowLayer.shadowOpacity = 1
shadowLayer.shadowOffset = CGSize(width: 0,height: 1)
shadowLayer.shadowColor = UIColor.redColor().CGColor
shadowLayer.shadowRadius = 1
shadowLayer.masksToBounds = false
shadowLayer.shadowPath = borderLine.CGPath
self.layer.addSublayer(shadowLayer)
What am I doing wrong as I dont seem to see anything wrong but of course I am wrong since no shadow appears. The function is drawRect, basic UIVIew no extra anything in there, x and y are the width and height of the frame. Many thanks in advance!
I take this example straight from my PaintCode-app. Hope this helps.
//// General Declarations
let context = UIGraphicsGetCurrentContext()
//// Shadow Declarations
let shadow = UIColor.blackColor()
let shadowOffset = CGSizeMake(3.1, 3.1)
let shadowBlurRadius: CGFloat = 5
//// Bezier 2 Drawing
var bezier2Path = UIBezierPath()
bezier2Path.moveToPoint(CGPointMake(30.5, 90.5))
bezier2Path.addLineToPoint(CGPointMake(115.5, 90.5))
CGContextSaveGState(context)
CGContextSetShadowWithColor(context, shadowOffset, shadowBlurRadius, (shadow as UIColor).CGColor)
UIColor.blackColor().setStroke()
bezier2Path.lineWidth = 1
bezier2Path.stroke()
CGContextRestoreGState(context)
I prefer the way to add a shadow-sublayer. You can easily use the following function (Swift 3.0):
func createShadowLayer() -> CALayer {
let shadowLayer = CALayer()
shadowLayer.shadowColor = UIColor.red.cgColor
shadowLayer.shadowOffset = CGSize.zero
shadowLayer.shadowRadius = 5.0
shadowLayer.shadowOpacity = 0.8
shadowLayer.backgroundColor = UIColor.clear.cgColor
return shadowLayer
}
And finally, you just add it to your line path (CAShapeLayer):
let line = CAShapeLayer()
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: 50, y: 100))
path.addLine(to: CGPoint(x: 100, y: 50))
line.path = path.cgPath
line.strokeColor = UIColor.blue.cgColor
line.fillColor = UIColor.clear.cgColor
line.lineWidth = 2.0
view.layer.addSublayer(line)
let shadowSubLayer = createShadowLayer()
shadowSubLayer.insertSublayer(line, at: 0)
view.layer.addSublayer(shadowSubLayer)
I am using the shadow properties of my shape layer to add shadow to it. The best part of this approach is that I don't have to provide a path explicitly. The shadow follows the path of the layer. I am also animating the layer by changing path. In that case too the shadow animates seamlessly without a single line of code.
Here is what I am doing (Swift 4.2)
shapeLayer.path = curveShapePath(postion: initialPosition)
shapeLayer.strokeColor = UIColor.clear.cgColor
shapeLayer.fillColor = shapeBackgroundColor
self.layer.addSublayer(shapeLayer)
if shadow {
shapeLayer.shadowRadius = 5.0
shapeLayer.shadowColor = UIColor.gray.cgColor
shapeLayer.shadowOpacity = 0.8
}
The curveShapePath method is the one that returns the path and is defined as follows:
func curveShapePath(postion: CGFloat) -> CGPath {
let height: CGFloat = 37.0
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: 0)) // start top left
path.addLine(to: CGPoint(x: (postion - height * 2), y: 0)) // the beginning of the trough
// first curve down
path.addCurve(to: CGPoint(x: postion, y: height),
controlPoint1: CGPoint(x: (postion - 30), y: 0), controlPoint2: CGPoint(x: postion - 35, y: height))
// second curve up
path.addCurve(to: CGPoint(x: (postion + height * 2), y: 0),
controlPoint1: CGPoint(x: postion + 35, y: height), controlPoint2: CGPoint(x: (postion + 30), y: 0))
// complete the rect
path.addLine(to: CGPoint(x: self.frame.width, y: 0))
path.addLine(to: CGPoint(x: self.frame.width, y: self.frame.height))
path.addLine(to: CGPoint(x: 0, y: self.frame.height))
path.close()
return path.cgPath
}