Draw a line with a gradient using NSBezierPath, CAShapeLayer, and CAGradientLayer - swift

I have a macOS app where I draw lines and other figures.
I use NSBezierPath to form the lines or figures and then add it to the CAShapeLayer's path and finally, add the CAShapeLayer to the view.
This all works fine and I have a single stroke color. However, I want the lines/figures not to be of a single color but of multiple colors(gradient). I know this can be done through CAGradientLayer and tried a few examples from other SO questions showing iOS variants but they didn't work for me.
Below is a code snippet from the app:
startPoint = point
currentPath = NSBezierPath()
currentShape = CAShapeLayer()
currentShape?.lineWidth = lineWeight
currentShape?.strokeColor = strokeColor.cgColor
currentShape?.fillColor = fillColor.cgColor
currentShape?.lineJoin = CAShapeLayerLineJoin.round
currentShape?.lineCap = CAShapeLayerLineCap.round
currentPath?.move(to: point)
currentPath?.line(to: point)
currentShape?.path = currentPath?.cgPath
view.layer?.addSublayer(currentShape!)
currentGradient = CAGradientLayer()
currentGradient?.mask = currentShape
currentGradient?.frame = currentShape!.frame
currentGradient?.colors = [NSColor.red.cgColor, NSColor.black.cgColor]
view.layer?.addSublayer(currentGradient!)
// some more code here that I omitted (to draw shapes with NSBezierPath)
if let shape = currentShape {
shape.path = currentPath?.cgPath
currentGradient?.mask = shape
currentGradient?.frame = shape.frame
}
If I remove the code that involves currentGradient then I see the lines/figures (in one solid color) but when I add the gradient part, I do not see anything. What am I doing wrong in the above code?
Any help or hints would be really appreciated.

If you have searched, then you know the answer. You cannot stroke a shape with a gradient. Instead, you mask to a gradient; the shape layer becomes the mask whereby the gradient layer is revealed. Example (v is a view in the window):
let grad = CAGradientLayer()
grad.frame = v.bounds
v.layer?.addSublayer(grad)
grad.colors = [NSColor.red.cgColor, NSColor.blue.cgColor]
let shape = CAShapeLayer()
shape.frame = v.bounds
shape.lineWidth = 3
shape.fillColor = NSColor.clear.cgColor
shape.strokeColor = NSColor.black.cgColor
let path = CGPath(ellipseIn: v.bounds.insetBy(dx: 4, dy: 4), transform: nil)
shape.path = path
grad.mask = shape

Related

shadow on bezierpath view adds weird strokes to corners

I wanted to recreate the AppStore's "today" cards with rounded corners and a light drop shadow.
I created a path, a maskLayer and a separate shadowLayer, which – according to several sources – is the way of doing it.
The problem, however, is that my lovely rounded rectangle with a shadow has got some gray strokes at it corners. How can I solve this? I tried different shadow opacities and different radii. It didn't solve my problem.
Here you can see my screenshots and my code below.
override func loadView() {
let view = UIView()
view.backgroundColor = .white
self.view = view
// create sample view and add to view hierarchy
let bigTeaser = UIView(frame: CGRect(x: 16, y: 200, width: 343, height: 267))
bigTeaser.backgroundColor = UIColor.white
view.addSubview(bigTeaser)
// create the path for the rounded corners and the shadow
let roundPath = UIBezierPath(roundedRect: bigTeaser.bounds, cornerRadius: 20)
// create maskLayer
let maskLayer = CAShapeLayer()
maskLayer.frame = bigTeaser.bounds
maskLayer.path = roundPath.cgPath
bigTeaser.layer.mask = maskLayer
// create shadowLayer
let shadowLayer = CAShapeLayer()
shadowLayer.path = roundPath.cgPath
shadowLayer.frame = bigTeaser.frame
shadowLayer.shadowOpacity = 0.3
shadowLayer.shadowRadius = 24
shadowLayer.shadowColor = UIColor.black.cgColor
shadowLayer.shadowOffset = CGSize(width: 0, height: 2)
// insert layers
bigTeaser.superview!.layer.insertSublayer(shadowLayer, below: bigTeaser.layer)
}
If you replace:
shadowLayer.path = roundPath.cgPath
to
shadowLayer.shadowPath = roundPath.cgPath
The ugly borders will magically disappear.

Swift - how to animatedly draw line NOT rect? Can only draw rect?

I have looked around about this before but have yet to find an answer. I've been at this for a while and need to know how to animate a LINE as opposed to a rectangle.
From what I can see the animation is very different going from a stroke to a box. Just need some pointers here-
I have a border that is drawn on view load (not with an animation, it just draws it) here:
override public func drawRect(rect: CGRect){
super.drawRect(rect)
let borderColor = self.hasError! ? kDefaultActiveColor : kDefaultErrorColor
let textRect = self.textRectForBounds(rect)
let context = UIGraphicsGetCurrentContext()
let borderlines : [CGPoint] = [CGPointMake(0, CGRectGetHeight(textRect) - 1),
CGPointMake(CGRectGetWidth(textRect), CGRectGetHeight(textRect) - 1)]
if self.enabled {
CGContextBeginPath(context);
CGContextAddLines(context, borderlines, 2);
CGContextSetLineWidth(context, 1.3);
CGContextSetStrokeColorWithColor(context, borderColor.CGColor);
CGContextStrokePath(context);
I got this from another answer and am trying to take it apart. I need the line to animate/grow out from its centre when the view loads, as in grow to the same width and size as it is drawn here but animated.
I do not know how to accomplish this, however another answer achieved the effect I need by setting the border to a size of zero here:
self.activeBorder = UIView(frame: CGRectZero)
self.activeBorder.backgroundColor = kDefaultActiveColor
//self.activeBorder.backgroundColor = kDefaultActiveBorderColor
self.activeBorder.layer.opacity = 0
self.addSubview(self.activeBorder)
and animating using:
self.borderlines.transform = CATransform3DMakeScale(CGFloat(0.01), CGFloat(1.0), 1)
self.activeBorder.layer.opacity = 1
CATransaction.begin()
self.activeBorder.layer.transform = CATransform3DMakeScale(CGFloat(0.03), CGFloat(1.0), 1)
let anim2 = CABasicAnimation(keyPath: "transform")
let fromTransform = CATransform3DMakeScale(CGFloat(0.01), CGFloat(1.0), 1)
let toTransform = CATransform3DMakeScale(CGFloat(1.0), CGFloat(1.0), 1)
anim2.fromValue = NSValue(CATransform3D: fromTransform)
anim2.toValue = NSValue(CATransform3D: toTransform)
anim2.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
anim2.fillMode = kCAFillModeForwards
anim2.removedOnCompletion = false
self.activeBorder.layer.addAnimation(anim2, forKey: "_activeBorder")
CATransaction.commit()
This seems to be using entirely different code than my original drawRect and I don't know how to combine the two. Where am I going wrong here? How can I draw out from the centre my first border as I do the second, active border?
Use CAShapeLater, UIBezierPath and CABasicAnimation to animatedly draw line.
Here is the sample code:
class SampleView: UIView {
override public func drawRect(rect: CGRect) {
let leftPath = UIBezierPath()
leftPath.moveToPoint(CGPointMake(CGRectGetMidX(frame), CGRectGetHeight(frame)))
leftPath.addLineToPoint(CGPointMake(0, CGRectGetHeight(frame)))
let rightPath = UIBezierPath()
rightPath.moveToPoint(CGPointMake(CGRectGetMidX(frame), CGRectGetHeight(frame)))
rightPath.addLineToPoint(CGPointMake(CGRectGetWidth(frame), CGRectGetHeight(frame)))
let leftShapeLayer = CAShapeLayer()
leftShapeLayer.path = leftPath.CGPath
leftShapeLayer.strokeColor = UIColor.redColor().CGColor;
leftShapeLayer.lineWidth = 1.3
leftShapeLayer.strokeEnd = 0
layer.addSublayer(leftShapeLayer)
let rightShpaeLayer = CAShapeLayer()
rightShpaeLayer.path = rightPath.CGPath
rightShpaeLayer.strokeColor = UIColor.redColor().CGColor;
rightShpaeLayer.lineWidth = 1.3
rightShpaeLayer.strokeEnd = 0
layer.addSublayer(rightShpaeLayer)
let drawLineAnimation = CABasicAnimation(keyPath: "strokeEnd")
drawLineAnimation.toValue = NSNumber(float: 1)
drawLineAnimation.duration = 1
drawLineAnimation.fillMode = kCAFillModeForwards
drawLineAnimation.removedOnCompletion = false
leftShapeLayer.addAnimation(drawLineAnimation, forKey: nil)
rightShpaeLayer.addAnimation(drawLineAnimation, forKey: nil)
}
}
The code in viewDidLoad of ViewController
// ...
let sampleView = SampleView()
sampleView.frame = CGRectMake(10, 60, 240, 50)
sampleView.backgroundColor = UIColor.lightGrayColor()
view.addSubview(sampleView)
Here is the capture:
Note: If you run the code in iOS9 Simulator (I have not tested the earlier version), the animation may get blocked. Choose to run it in a real device or in iOS10 Simulator.

Drawing a gradient color in an arc with a rounded edge

I want to replicate this in Swift.. reading further: How to draw a linear gradient arc with Qt QPainter?
I've figured out how to draw a gradient inside of a rectangular view:
override func drawRect(rect: CGRect) {
let currentContext = UIGraphicsGetCurrentContext()
CGContextSaveGState(currentContext)
let colorSpace = CGColorSpaceCreateDeviceRGB()
let startColor = MyDisplay.displayColor(myMetric.currentValue)
let startColorComponents = CGColorGetComponents(startColor.CGColor)
let endColor = MyDisplay.gradientEndDisplayColor(myMetric.currentValue)
let endColorComponents = CGColorGetComponents(endColor.CGColor)
var colorComponents
= [startColorComponents[0], startColorComponents[1], startColorComponents[2], startColorComponents[3], endColorComponents[0], endColorComponents[1], endColorComponents[2], endColorComponents[3]]
var locations: [CGFloat] = [0.0, 1.0]
let gradient = CGGradientCreateWithColorComponents(colorSpace, &colorComponents, &locations, 2)
let startPoint = CGPointMake(0, 0)
let endPoint = CGPointMake(1, 8)
CGContextDrawLinearGradient(currentContext, gradient, startPoint, endPoint, CGGradientDrawingOptions.DrawsAfterEndLocation)
CGContextRestoreGState(currentContext)
}
and how to draw an arc
func drawArc(color: UIColor, x: CGFloat, y: CGFloat, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat, lineWidth: CGFloat, clockwise: Int32) {
let context = UIGraphicsGetCurrentContext()
CGContextSetLineWidth(context, lineWidth)
CGContextSetStrokeColorWithColor(context,
color.CGColor)
CGContextAddArc(context, x, y, radius, startAngle, endAngle, clockwise)
CGContextStrokePath(context)
}
With the gradient, that took up the entire view. I was unsure how to limit the gradient to a particular area so I made a subclass of UIView, modified the frame and used the gradient code (first snippet).
For arcs, I've used the second snippet. I'm thinking I can draw an arc and then draw a circle at the end of the arc to smooth out the arc with a rounded finish. How can I do this and draw a gradient over the arc shape?
Please ignore the purple mark. The first picture is the beginning of the gradient arc from my specification, the second is the end of the gradient arc, below and to the left of the beginning arc.
How can I draw this in Swift with Core Graphics?
Exact code for 2019 with true circular gradient and totally flexible end positions:
These days basically you simply have a gradient layer and mask for that layer.
So step one, you need a "mask".
What the heck is a "mask" in iOS?
In iOS, you actually use a CAShapeLayer to make a mask.
This is confusing. It should be called something like 'CAShapeToUseAsAMaskLayer'
But that's how you do it, a CAShapeLayer.
Step two. In layout, you must resize the shape layer. And that's it.
So basically it's just this:
class SweetArc: UIView {
open override func layoutSubviews() {
super.layoutSubviews()
arcLayer.frame = bounds
gradientLayer.frame = bounds
}
private lazy var gradientLayer: CAGradientLayer = {
let l = CAGradientLayer()
...
l.mask = arcLayer
layer.addSublayer(l)
return l
}()
private lazy var arcLayer: CAShapeLayer = {
let l = CAShapeLayer()
...
return l
}()
}
That's the whole thing.
Note that the mask of the gradient layer is indeed set as the shape layer, which we have named "arcLayer".
A shape layer is used as a mask.
Here's the exact code for the two layers:
private lazy var gradientLayer: CAGradientLayer = {
// recall that .conic is finally available in 12 !
let l = CAGradientLayer()
l.type = .conic
l.colors = [
UIColor.yellow,
UIColor.red
].map{$0.cgColor}
l.locations = [0, 1]
l.startPoint = CGPoint(x: 0.5, y: 0.5)
l.endPoint = CGPoint(x: 0.5, y: 0)
l.mask = arcLayer
layer.addSublayer(l)
return l
}()
private lazy var arcLayer: CAShapeLayer = {
let l = CAShapeLayer()
l.path = arcPath
// to use a shape layer as a mask, you mask with white-on-clear:
l.fillColor = UIColor.clear.cgColor
l.strokeColor = UIColor.white.cgColor
l.lineWidth = lineThick
l.lineCap = CAShapeLayerLineCap.round
l.strokeStart = 0.4 // try values 0-1
l.strokeEnd = 0.5 // try values 0-1
return l
}()
Again, notice arcLayer is a shape layer you are using as a mask - you do NOT actually add it as a subview.
Remember that shape layers used as masks do not! get added as subviews.
That can be a source of confusion. It's not a "layer" at all.
Finally, notice arcLayer uses a path arcPath.
Construct the path perfectly so that strokeStart and strokeEnd work correctly.
You want to build the path perfectly.
So that it is very easy to set the stroke start and stroke end values.
And they make sense by perfectly and correctly running clockwise, from the top, from 0 to 1.
Here's the exact code for that:
private let lineThick: CGFloat = 10.0
private let outerGap: CGFloat = 10.0
private let beginFraction: CGFloat = 0 // 0 means from top; runs clockwise
private lazy var arcPath: CGPath = {
let b = beginFraction * .pi * 2.0
return UIBezierPath(
arcCenter: bounds.centerOfCGRect(),
radius: bounds.width / 2.0 - lineThick / 2.0 - outerGap,
startAngle: .pi * -0.5 + b,
endAngle: .pi * 1.5 + b,
clockwise: true
).cgPath
}()
You're done!
override func draw(_ rect: CGRect) {
//Add gradient layer
let gl = CAGradientLayer()
gl.frame = rect
gl.colors = [UIColor.red.cgColor, UIColor.blue.cgColor]
layer.addSublayer(gl)
//create mask in the shape of arc
let sl = CAShapeLayer()
sl.frame = rect
sl.lineWidth = 15.0
sl.strokeColor = UIColor.red.cgColor
let path = UIBezierPath()
path.addArc(withCenter: .zero, radius: 250.0, startAngle: 10.0.radians, endAngle: 60.0.radians, clockwise: true)
sl.fillColor = UIColor.clear.cgColor
sl.lineCap = kCALineCapRound
sl.path = path.cgPath
//Add mask to gradient layer
gl.mask = sl
}
Hope this helps!!

Swift: Rounded View with inner shadow

I am trying to implement the solution here (which I converted to Swift): How to create rounded UITextField with inner shadow
My code is as follows:
mobileInputView.layer.cornerRadius = 5.0;
let shadowLayer = CAShapeLayer()
shadowLayer.frame = mobileInputView.bounds;
// Standard shadow stuff
shadowLayer.shadowColor = UIColor(white: 0, alpha: 1).CGColor
shadowLayer.shadowOffset = CGSizeMake(0.0, 0.0)
shadowLayer.shadowOpacity = 1.0
shadowLayer.shadowRadius = 4
// Causes the inner region in this example to NOT be filled.
shadowLayer.fillRule = kCAFillRuleEvenOdd
// Create the larger rectangle path.
let path = CGPathCreateMutable();
CGPathAddRect(path, nil, CGRectInset(mobileInputView.bounds, -42, -42));
// Add the inner path so it's subtracted from the outer path.
// someInnerPath could be a simple bounds rect, or maybe
// a rounded one for some extra fanciness.
let someInnerPath = UIBezierPath(roundedRect: mobileInputView.bounds, cornerRadius:5.0).CGPath
CGPathAddPath(path, nil, someInnerPath)
CGPathCloseSubpath(path);
shadowLayer.path = path
//CGPathRelease(path);
mobileInputView.layer.addSublayer(shadowLayer)
//[[_textField layer] addSublayer:shadowLayer];
let maskLayer = CAShapeLayer()
maskLayer.path = someInnerPath
shadowLayer.mask = maskLayer
But the view ends up coming out like this:
The width of the component is correct, I can type until the edge of the screen, it's just the border that comes out a bad size. If I set the background colour of the view I can see the view itself is being sized correctly.
If I try using .frame instead of .bounds I get the same result.
A much simpler way to achieve this is to modify the UITextField directly without have the outlining UIView and the label:
let label = UILabel(frame: CGRectMake(1, 1, 125, 25))
label.text = "Mobile number:"
label.textAlignment = NSTextAlignment.Right
mobileNumber.leftView = label
mobileNumber.leftViewMode = UITextFieldViewMode.Always
mobileNumber.borderStyle = UITextBorderStyle.Bezel;
mobileNumber.layer.borderWidth = 1.0
mobileNumber.layer.borderColor = UIColor.grayColor().CGColor
mobileNumber.layer.cornerRadius = 5.0

Moving an object along a curve in iPhone development

I wanted to animate an image object by moving it along a particular curve. It is not a general or random curve but rather a curve which follows a particular path on screen.
Currently, Im manually specifying the list of x and y co-ordinates of the path along which i want the image object to move by setting its frame each time. This is a laborious process in the sense that im setting the specific x and y coordinates of the path and moving the image along it. Is there a more efficient way to do this?
Is there a way that i can specify,say, just about 15 - 20 points and have a curve traced along those to move the object? Any other way to acheive this? Any help would be much appreciated. Thanks.
You could use a combination of UIBezierPath and CAKeyFrameAnimation.
I found a very useful blog post dealing with this subject.
http://oleb.net/blog/2010/12/animating-drawing-of-cgpath-with-cashapelayer/
Here's a simplified version of what I used (it just animates the drawing of a square):
UIBezierPath *customPath = [UIBezierPath bezierPath];
[customPath moveToPoint:CGPointMake(100,100)];
[customPath addLineToPoint:CGPointMake(200,100)];
[customPath addLineToPoint:CGPointMake(200,200)];
[customPath addLineToPoint:CGPointMake(100,200)];
[customPath addLineToPoint:CGPointMake(100,100)];
UIImage *movingImage = [UIImage imageNamed:#"foo.png"];
CALayer *movingLayer = [CALayer layer];
movingLayer.contents = (id)movingImage.CGImage;
movingLayer.anchorPoint = CGPointZero;
movingLayer.frame = CGRectMake(0.0f, 0.0f, movingImage.size.width, movingImage.size.height);
[self.view.layer addSublayer:movingLayer];
CAKeyframeAnimation *pathAnimation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
pathAnimation.duration = 4.0f;
pathAnimation.path = customPath.CGPath;
pathAnimation.calculationMode = kCAAnimationLinear;
[movingLayer addAnimation:pathAnimation forKey:#"movingAnimation"];
In Swift-3 version of #Jilouc :-
override func viewDidLoad() {
super.viewDidLoad()
addAdditiveAnimation()
initiateAnimation()
}
//curve which follows a particular path
func pathToTrace() -> UIBezierPath {
let path = UIBezierPath(ovalIn: CGRect(x: 120 , y: 120, width: 100, height: 100))
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = 1.0
self.view.layer.addSublayer(shapeLayer)
return path
}
func addAdditiveAnimation() {
let movement = CAKeyframeAnimation(keyPath: "position")
movement.path = pathToTrace().cgPath
movement.duration = 5
movement.repeatCount = HUGE
movement.calculationMode = kCAAnimationPaced
movement.timingFunctions = [CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)]
self.movement = movement
}
func createLayer() -> CALayer {
let layer = CALayer()
let image = UIImage(named: "launch.png")
layer.frame = CGRect(x: 0 , y: 0, width: (image?.size.width),height: (image?.size.height))
layer.position = CGPoint(x: 5, y: 5)
layer.contents = image?.cgImage
layer.anchorPoint = .zero
layer.backgroundColor = UIColor.red.cgColor
//layer.cornerRadius = 5
self.view.layer.addSublayer(layer)
return layer
}
func initiateAnimation() {
let layer = createLayer()
layer.add(self.movement, forKey: "Object Movement")
}
Github Demo