Efficient resize the CAShapeLayer if the view grows animatable - swift

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:

Related

Swift Animate Mask Transition

How can you animate a mask moving down X pixels?
I have a circular mask applied to a black view that creates a spotlight effect. I want to animate the mask transitioning down 100 pixels, but using UIView.animate { mask.transform = CATransform3DTranslate(.init(), 0, 100, 0) } instantly moves it instead of animating the transition.
Here's the mask code:
private lazy var mask: CAShapeLayer = {
let mask = CAShapeLayer()
let path = CGMutablePath()
path.addArc(center: CGPoint(x: 0, y: 0), radius: 40, startAngle: 0, endAngle: 2.0 * .pi, clockwise: false)
path.addRect(CGRect(origin: .zero, size: view.frame.size))
mask.backgroundColor = UIColor.black.cgColor
mask.path = path
mask.fillRule = .evenOdd
return mask
}()
overlayView.layer.mask = mask
How would you animate this moving down 100 pixels?
To get the spotlight effect, you'll want to create paths that modify the arc shape and animate between them.
func spotlightPath(arcCenter: CGPoint) -> CGPath {
let path = CGMutablePath()
// move the spotlight
path.addArc(center: CGPoint(x: arcCenter.x, y: arcCenter.y), radius: 40, startAngle: 0, endAngle: 2.0 * .pi, clockwise: false)
path.addRect(CGRect(origin: .zero, size: view.frame.size))
return path
}
let startPath = spotlightPath(arcCenter: .zero)
And then later
let endPath = spotlightPath(arcCenter: CGPoint(x: 40, y: 100))
let animation = CABasicAnimation(keyPath: "path")
animation.duration = 1
animation.isRemovedOnCompletion = false
animation.fillMode = .forwards
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
animation.toValue = endPath
mask.add(animation, forKey: "path")
Try this Core animation block. CALayer animation is not supported by UIView animation block. To animate mask property we use the path key.
let anim = CABasicAnimation(keyPath: "path")
anim.toValue = createPath(center: .init(x: 100, y: 100))
anim.duration = 1
anim.fillMode = .forwards
anim.timingFunction = CAMediaTimingFunction.init(name: .easeInEaseOut)
anim.isRemovedOnCompletion = false
mask.add(anim, forKey: anim.keyPath)

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

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

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.

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.