"path" animation with CABasicAnimation - Turn "x" into a checkmark - swift

I want to animate a 'morph' from "x" to a checkmark: So this:
should turn into this (animated):
I created those paths separately (see code below) but when I try to animate them with CABasicAnimation I get this instead of the cross at the beginning:
And this then turns into the checkmark. Why? And how can I fix it? Or doesn't this work with CABasicAnimation?
// just the blue background
let shapeLayer = CAShapeLayer()
shapeLayer.frame = CGRect(x: 0, y: 0, width: bounds.height, height: bounds.height)
shapeLayer.backgroundColor = UIColor.blue.cgColor
layer.addSublayer(shapeLayer)
// first path (the "x")
let padding: CGFloat = 10
let crossPath = UIBezierPath()
crossPath.move(to: CGPoint(x: padding, y: padding))
crossPath.addLine(to: CGPoint(x: bounds.height - padding, y: bounds.height - padding))
crossPath.move(to: CGPoint(x: bounds.height - padding, y: padding))
crossPath.addLine(to: CGPoint(x: padding, y: bounds.height - padding))
// Create the shape layer that will draw the path
let signLayer = CAShapeLayer()
signLayer.frame = bounds
signLayer.path = crossPath.cgPath // setting the path from above
signLayer.strokeColor = UIColor.white.cgColor
signLayer.fillColor = UIColor.clear.cgColor
signLayer.lineWidth = 4
// add it to your view's layer and you're done!
layer.addSublayer(signLayer)
let anim: CABasicAnimation = CABasicAnimation(keyPath: "path")
anim.duration = 10.0 // just a test value to see the animation more clearly
// the checkmark
let checkmarkPath = UIBezierPath()
checkmarkPath.move(to: CGPoint(x: bounds.height - padding, y: padding))
checkmarkPath.addLine(to: CGPoint(x: 2 * padding, y: bounds.height - padding))
checkmarkPath.addLine(to: CGPoint(x: padding, y: bounds.height / 2))
anim.fromValue = crossPath.cgPath
anim.toValue = checkmarkPath.cgPath
signLayer.path = checkmarkPath.cgPath
signLayer.add(anim, forKey: "path")

To animate correctly, you need to animate between 2 paths with the same number of points. Your crossPath has 4 points and checkmarkPath has 3.

Related

Applying gradient layer over a UIBezierPath graph

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

Efficient resize the CAShapeLayer if the view grows animatable

I have a UIButton with a CAShapeLayer inside it. This is part of the subclass's code:
private func addBeneathFill(){
didAddedBeneathFill = true
let halfBeneathFill = UIBezierPath()
halfBeneathFill.move(to: CGPoint(x: 0, y: frame.height / 2))
halfBeneathFill.addCurve(to: CGPoint(x: frame.width, y: frame.height / 2), controlPoint1: CGPoint(x: frame.width / 10, y: frame.height / 2 + frame.height / 10), controlPoint2: CGPoint(x: frame.width, y: frame.height / 2 + frame.height / 10))
halfBeneathFill.addLine(to: CGPoint(x: frame.width, y: frame.height))
halfBeneathFill.addLine(to: CGPoint(x: 0, y: frame.height))
halfBeneathFill.addLine(to: CGPoint(x: 0, y: frame.height / 2))
let path = halfBeneathFill.cgPath
let shapeLayer = CAShapeLayer()
shapeLayer.path = path
let fillColor = halfBeneathFillColor.cgColor
shapeLayer.fillColor = fillColor
shapeLayer.opacity = 0.3
layer.insertSublayer(shapeLayer, at: 2)
}
It works great as long as the UIButton does not change in size. When it changes in size, the CAShapeLayer just stays in it's original position. Of course I can run the method again when the UIButton's frame has been changed, but in an animation it looks awful.
How can I correctly animate the CAShapeLayer's size to be always the current size of the UIButton?
You need to recalculate and animate your path on button frame change.
private func animateShapeLayer() {
let animation = CABasicAnimation(keyPath: "path")
animation.fromValue = getBezierPath(forFrame: oldFrame)
animation.toValue = getBezierPath(forFrame: newFrame)
animation.duration = 1.0
animation.fillMode = kCAFillModeForwards
animation.isRemovedOnCompletion = false
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
shapeLayer.add(animation, forKey: "path")
}
I've tested it, here you have a link to the playground gist and achieved result:

CATransform3DRotate applied to layer doesnt work

Could you please advice why CATransform3DRotate doesnt work for layer in my case, its blue layer on the image.
I have custom view, where I draw nature view, I want to animate changing the moon phase that will be done inside the white circle layer as its mask. I suppose that it is good idea to apply here 3DRotation, but for some reason it doesn't work even without animation, could be please advice what I am doing wrong?
func drawMoonPhase(inRect rect:CGRect, inContext context: CGContext) {
let moonShape = UIBezierPath(ovalIn: rect)
moonShape.lineWidth = 4.0
UIColor.white.setStroke()
moonShape.stroke()
moonShape.close()
let moonLayer = CAShapeLayer()
moonLayer.path = moonShape.cgPath
moonLayer.opacity = 0
self.layer.addSublayer(moonLayer)
let circlePath = UIBezierPath(ovalIn: rect)
UIColor.blue.setFill()
circlePath.fill()
circlePath.close()
let circleShape = CAShapeLayer()
circleShape.path = circlePath.cgPath
circleShape.opacity = 0
var transform = CATransform3DIdentity
transform.m34 = -1 / 500.0
transform = CATransform3DRotate(transform, (CGFloat(Double.pi * 0.3)), 0, 1, 0)
circleShape.transform = transform
moonLayer.mask = circleShape
}
Thanks in advance
EDIT:
Maybe I want clear , the effect i want is the below:
The transform occurs around the layer's anchor point that by default is in the center. Therefore what it is happening there is that the shape rotates around itself causing no visible result. :)
what you should do in this layout of layer is to use cos and sin math functions in order to determine the x and y position of your moon.
Let me know if you need more insights I will be happy to help.
also, please note that you don't need 2 shapeLayers in order to have the blue moon with the white stroke. CAShapeLayer has properties for both fill and stroke so you can simplify your code.
Based on the new info here is my new answer:
I was not able to get a nice effect by using the transform, so I decided to write the mask manually. this is the result:
/**
Draw a mask based on the moon phase progress.
- parameter rect: The rect where the mon will be drawn
- parameter progress: The progress of the moon phase. This value must be between 0 and 1
*/
func moonMask(in rect: CGRect, forProgress progress: CGFloat)->CALayer {
let path = CGMutablePath()
let center = CGPoint(x: rect.midX, y: rect.midY)
path.move(to: CGPoint(x: rect.midX, y: 0))
let relativeProgress = (max(min(progress, 1), 0) - 0.5) * 2
let radius = rect.width/2
let tgX = rect.midX+(relativeProgress * (radius*4/3))
path.addCurve(to: CGPoint(x: rect.midX, y: rect.maxY), control1: CGPoint(x: tgX, y: 0), control2: CGPoint(x: tgX, y: rect.maxY))
path.addArc(center: center, radius: rect.width/2, startAngle: .pi/2, endAngle: .pi*3/2, clockwise: false)
//path.closeSubpath()
let mask = CAShapeLayer()
mask.path = path
mask.fillColor = UIColor.black.cgColor
return mask
}
The function above draws a shapelier that can be used as mask for your moonLayer. This layer will be drawnin relation to a progress parameter that you will pass in the function where 1 is full moon and 0 is new moon.
You can put everything together to have the desired effect, and you can extract the path creation code to make a nice animation if you want.
This should answer your question I hope.
To quick test I wrote this playground:
import UIKit
let rect = CGRect(origin: .zero, size: CGSize(width: 200, height: 200))
let view = UIView(frame: rect)
view.backgroundColor = .black
let layer = view.layer
/**
Draw a mask based on the moon phase progress.
- parameter rect: The rect where the mon will be drawn
- parameter progress: The progress of the moon phase. This value must be between 0 and 1
*/
func moonMask(in rect: CGRect, forProgress progress: CGFloat)->CALayer {
let path = CGMutablePath()
let center = CGPoint(x: rect.midX, y: rect.midY)
path.move(to: CGPoint(x: rect.midX, y: 0))
let relativeProgress = (max(min(progress, 1), 0) - 0.5) * 2
let radius = rect.width/2
let tgX = rect.midX+(relativeProgress * (radius*4/3))
path.addCurve(to: CGPoint(x: rect.midX, y: rect.maxY), control1: CGPoint(x: tgX, y: 0), control2: CGPoint(x: tgX, y: rect.maxY))
path.addArc(center: center, radius: rect.width/2, startAngle: .pi/2, endAngle: .pi*3/2, clockwise: false)
//path.closeSubpath()
let mask = CAShapeLayer()
mask.path = path
mask.fillColor = UIColor.white.cgColor
return mask
}
let moonLayer = CAShapeLayer()
moonLayer.path = UIBezierPath(ovalIn: rect).cgPath
moonLayer.fillColor = UIColor.clear.cgColor
moonLayer.strokeColor = UIColor.white.cgColor
moonLayer.lineWidth = 2
moonLayer.shadowColor = UIColor.white.cgColor
moonLayer.shadowOpacity = 1
moonLayer.shadowRadius = 10
moonLayer.shadowPath = moonLayer.path
moonLayer.shadowOffset = .zero
layer.addSublayer(moonLayer)
let moonPhase = moonMask(in: rect, forProgress: 0.3)
moonPhase.shadowColor = UIColor.white.cgColor
moonPhase.shadowOpacity = 1
moonPhase.shadowRadius = 10
moonPhase.shadowPath = moonLayer.path
moonPhase.shadowOffset = .zero
layer.addSublayer(moonPhase)
view

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
}

iOS Camera Customization: How to implement Grid Lines?

I am able to use the flash feature, access the photo gallery, and use the rear/front camera.
I would like to implement grid lines that display when the user is taking a photo.
Any ideas?
Create a UIImageView to use for the cameraOverlayView.
Assuming you've got a UIImagePickerController named yourImagePickerController and also that you've got an image file named overlay.png as your 'grid lines'. When making your grid line image file, be sure to use a transparent background - not opaque white.
UIImageView *overlayImage = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"overlay.png"]];
CGRect overlayRect = CGRectMake(0, 0, overlayImage.image.size.width, overlayImage.image.size.height);
[overlayImage setFrame:overlayRect];
[yourImagePickerController setCameraOverlayView:overlayImage];
It's pretty easy with UIBezierPath. This is how you can achieve.
Save this code in a file called GridView.swift.
import Foundation
import UIKit
class GridView: UIView {
override func draw(_ rect: CGRect) {
let firstColumnPath = UIBezierPath()
firstColumnPath.move(to: CGPoint(x: bounds.width / 3, y: 0))
firstColumnPath.addLine(to: CGPoint(x: bounds.width / 3, y: bounds.height))
let firstColumnLayer = gridLayer()
firstColumnLayer.path = firstColumnPath.cgPath
layer.addSublayer(firstColumnLayer)
let secondColumnPath = UIBezierPath()
secondColumnPath.move(to: CGPoint(x: (2 * bounds.width) / 3, y: 0))
secondColumnPath.addLine(to: CGPoint(x: (2 * bounds.width) / 3, y: bounds.height))
let secondColumnLayer = gridLayer()
secondColumnLayer.path = secondColumnPath.cgPath
layer.addSublayer(secondColumnLayer)
let firstRowPath = UIBezierPath()
firstRowPath.move(to: CGPoint(x: 0, y: bounds.height / 3))
firstRowPath.addLine(to: CGPoint(x: bounds.width, y: bounds.height / 3))
let firstRowLayer = gridLayer()
firstRowLayer.path = firstRowPath.cgPath
layer.addSublayer(firstRowLayer)
let secondRowPath = UIBezierPath()
secondRowPath.move(to: CGPoint(x: 0, y: ( 2 * bounds.height) / 3))
secondRowPath.addLine(to: CGPoint(x: bounds.width, y: ( 2 * bounds.height) / 3))
let secondRowLayer = gridLayer()
secondRowLayer.path = secondRowPath.cgPath
layer.addSublayer(secondRowLayer)
}
private func gridLayer() -> CAShapeLayer {
let shapeLayer = CAShapeLayer()
shapeLayer.strokeColor = UIColor(white: 1.0, alpha: 0.3).cgColor
shapeLayer.frame = bounds
shapeLayer.fillColor = nil
return shapeLayer
}
}
Usage:
func addGridView(cameraView: UIView) {
let horizontalMargin = cameraView.bounds.size.width / 4
let verticalMargin = cameraView.bounds.size.height / 4
let gridView = GridView()
gridView.translatesAutoresizingMaskIntoConstraints = false
cameraView.addSubview(gridView)
gridView.backgroundColor = UIColor.clear
gridView.leftAnchor.constraint(equalTo: cameraView.leftAnchor, constant: horizontalMargin).isActive = true
gridView.rightAnchor.constraint(equalTo: cameraView.rightAnchor, constant: -1 * horizontalMargin).isActive = true
gridView.topAnchor.constraint(equalTo: cameraView.topAnchor, constant: verticalMargin).isActive = true
gridView.bottomAnchor.constraint(equalTo: cameraView.bottomAnchor, constant: -1 * verticalMargin).isActive = true
}
As far as the documentation goes it doesn't state whether the grid lines provided by Apple is actually a shared method but as it's not mentioned I'd say not, but you can implement your own with the cameraOverlayView.