Animate different corners in CAShapeLayer - swift

I am using the following code:
let MainLayer : CAShapeLayer = {
let mainlayer = CAShapeLayer()
mainlayer.fillColor = UIColor.white.cgColor
mainlayer.shadowColor = UIColor.darkGray.cgColor
mainlayer.shadowOffset = CGSize(width: 3.0, height: 3.0)
mainlayer.shadowOpacity = 0.5
mainlayer.shadowRadius = 7
return mainlayer
}()
var LayerRect:CGRect!
var bezierPath:UIBezierPath!
let CellHeightMargin:CGFloat = 6.0
let CellWidthMargin:CGFloat = 15.0
override func viewDidLoad() {
super.viewDidLoad()
LayerRect = CGRect(x: self.bounds.origin.x, y: self.bounds.origin.y, width: UIScreen.main.bounds.width - CellWidthMargin * 2, height: self.bounds.height - CellHeightMargin * 2)
bezierPath = UIBezierPath(roundedRect: LayerRect, byRoundingCorners: [.bottomRight,.topRight], cornerRadii: CGSize(width: 5, height: 5))
MainLayer.path = bezierPath.cgPath
self.mainView.layer.insertSublayer(MainLayer, at: 0)
}
As you can see, byRoundingCorners is to bottom right and top right corners, now I want to replace it by the bottom left and bottom right with animation. How to do that?

By the following:
let ToStatusLayerPath = UIBezierPath(roundedRect: CellView.statusView.bounds, byRoundingCorners: [.bottomRight,.bottomLeft], cornerRadii: CGSize(width: 5, height: 5))
let animation = CABasicAnimation(keyPath: "path")
animation.toValue = ToStatusLayerPath.cgPath
animation.duration = 1.0;
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
animation.fillMode = kCAFillModeForwards
animation.isRemovedOnCompletion = false
statusLayer.add(animation, forKey: nil)

Related

Swift how to mask shape layer to blur layer

I was making a progress circle, and I want its track path to have a blur effect, is there any way to achieve that?
This is what the original track looks like(the track path is transparent, I want it to be blurred)
And this is my attempt
let outPutViewFrame = CGRect(x: 0, y: 0, width: 500, height: 500)
let circleRadius: CGFloat = 60
let circleViewCenter = CGPoint(x: outPutViewFrame.width / 2 , y: outPutViewFrame.height / 2)
let circleView = UIView()
let progressWidth: CGFloat = 8
circleView.frame.size = CGSize(width: (circleRadius + progressWidth) * 2, height: (circleRadius + progressWidth) * 2)
circleView.center = circleViewCenter
let circleTrackPath = UIBezierPath(arcCenter: CGPoint(x: circleView.frame.width / 2, y: circleView.frame.height / 2), radius: circleRadius, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)
let blur = UIBlurEffect(style: .light)
let blurEffect = UIVisualEffectView(effect: blur)
blurEffect.frame = circleView.bounds
blurEffect.mask(withPath: circleTrackPath, inverse: false)
extension UIView {
func mask(withPath path: UIBezierPath, inverse: Bool = false) {
let maskLayer = CAShapeLayer()
if inverse {
path.append(UIBezierPath(rect: self.bounds))
maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
}
maskLayer.path = path.cgPath
maskLayer.lineWidth = 5
maskLayer.lineCap = CAShapeLayerLineCap.round
self.layer.mask = maskLayer
}
}
Set maskLayer.fillRule to evenOdd, even when not inversed.
if inverse {
path.append(UIBezierPath(rect: self.bounds))
}
maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
create the circleTrackPath by using a big circle and a smaller circle.
let circleCenter = CGPoint(x: circleView.frame.width / 2, y: circleView.frame.height / 2)
let circleTrackPath = UIBezierPath(ovalIn:
CGRect(origin: circleCenter, size: .zero)
.insetBy(dx: circleRadius, dy: circleRadius))
// smaller circle
circleTrackPath.append(CGRect(origin: circleCenter, size: .zero)
.insetBy(dx: circleRadius * 0.8, dy: circleRadius * 0.8))
Set circleTrackPath.usesEvenOddFillRule to true:
circleTrackPath.usesEvenOddFillRule = true
Now you have a blurred full circle. The non-blurred arc part can be implemented as another sublayer.
Here is a MCVE that you can paste into a playground:
let container = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
// change this to a view of your choice
let image = UIImageView(image: UIImage(named: "my_image"))
let blur = UIVisualEffectView(effect: UIBlurEffect(style: .light))
container.addSubview(image)
blur.frame = image.frame
container.addSubview(blur)
let outer = image.bounds.insetBy(dx: 30, dy: 30)
let path = UIBezierPath(ovalIn: outer)
path.usesEvenOddFillRule = true
path.append(UIBezierPath(ovalIn: outer.insetBy(dx: 10, dy: 10)))
let maskLayer = CAShapeLayer()
maskLayer.path = path.cgPath
maskLayer.fillRule = .evenOdd
blur.layer.mask = maskLayer
container // <--- playground quick look this
Using my profile pic as the background, this produces:

How can I make chained animation using Core Animation in Swift?

I would like to do animation1 first, then animation2. I would also like to repeat them forever.
let animation1 = CAKeyframeAnimation(keyPath: #keyPath(CALayer.position))
animation1.path = circularFirstPath.cgPath
let animation2 = CAKeyframeAnimation(keyPath: #keyPath(CALayer.position))
animation2.path = circularSecondPath.cgPath
circleView3.layer.add(animation1, forKey: nil)
circleView3.layer.add(animation2, forKey: nil)
If you want to lead layer along path:
let segmentDuration: CFTimeInterval = 2.0
let rect = CGRect(x: 0.0, y: 0.0, width: 100.0, height: 100.0)
let path = UIBezierPath(rect: rect)
let posAnimation = CAKeyframeAnimation(keyPath: "position")
posAnimation.duration = segmentDuration
posAnimation.path = path.cgPath
posAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
posAnimation.calculationMode = .paced
posAnimation.repeatCount = .infinity
anyLayer.add(posAnimation, forKey: "position")
If you want to change position try something like this:
let segmentDuration: CFTimeInterval = 2.0
let points = [CGPoint(x: 0, y: 0), CGPoint(x: 400.0, y: 0.0), CGPoint(x: 200.0, y: 200.0)]
let posAnimation = CABasicAnimation(keyPath: "position")
posAnimation.duration = segmentDuration
posAnimation.fromValue = points[0]
posAnimation.toValue = points[1]
let posAnimation1 = CABasicAnimation(keyPath: "position")
posAnimation1.beginTime = posAnimation.duration
posAnimation1.duration = segmentDuration
posAnimation1.fromValue = points[1]
posAnimation1.toValue = points[2]
let posAnimation2 = CABasicAnimation(keyPath: "position")
posAnimation2.beginTime = posAnimation.duration + posAnimation1.duration
posAnimation2.duration = segmentDuration
posAnimation2.fromValue = points[2]
posAnimation2.toValue = points[0]
let positionChain: CAAnimationGroup = CAAnimationGroup()
positionChain.animations = [posAnimation, posAnimation1, posAnimation2]
positionChain.duration = posAnimation.duration + posAnimation1.duration + posAnimation2.duration
positionChain.fillMode = CAMediaTimingFillMode.forwards
positionChain.repeatCount = .infinity
anySubLayer.add(positionChain, forKey: "positionChain")
If you want to change shape of main layer just create CAShapeLayer with shape what you need, mask your layer and start animation:
let segmentDuration: CFTimeInterval = 2.0
let rect = CGRect(x: 0.0, y: 0.0, width: 100.0, height: 100.0)
let shapeLayer = CAShapeLayer()
shapeLayer.path = UIBezierPath(rect: rect).cgPath
shapeLayer.frame = CGRect(x: 0, y: 0, width: 10, height: 10)
layer.mask = shapeLayer
let paths = [UIBezierPath(rect: rect), UIBezierPath(roundedRect: rect, cornerRadius: 25), UIBezierPath(ovalIn: rect)]
let pathAnimation = CABasicAnimation(keyPath: "path")
pathAnimation.duration = segmentDuration
pathAnimation.fromValue = paths[0].cgPath
pathAnimation.toValue = paths[1].cgPath
let pathAnimation1 = CABasicAnimation(keyPath: "path")
pathAnimation1.beginTime = pathAnimation.duration
pathAnimation1.duration = segmentDuration
pathAnimation1.fromValue = paths[1].cgPath
pathAnimation1.toValue = paths[2].cgPath
let pathAnimation2 = CABasicAnimation(keyPath: "path")
pathAnimation2.beginTime = pathAnimation.duration + pathAnimation1.duration
pathAnimation2.duration = segmentDuration
pathAnimation2.fromValue = paths[2].cgPath
pathAnimation2.toValue = paths[0].cgPath
let pathChain: CAAnimationGroup = CAAnimationGroup()
pathChain.animations = [pathAnimation, pathAnimation1, pathAnimation2]
pathChain.duration = pathAnimation.duration + pathAnimation1.duration + pathAnimation2.duration
pathChain.fillMode = CAMediaTimingFillMode.forwards
pathChain.repeatCount = .infinity
shapeLayer.add(pathChain, forKey: "positionChain")

tap event does not fire on animated imageview

I want an animating imageView with UIBezierPath (sliding from top to bottom) to be clicked. But every google help I found didn't solve it. But it does not print "clicked".
The animation does work perfectly. Maybe anyone's got a clue?
Help is very appreciated.
override func viewDidLoad() {
super.viewDidLoad()
let imageView = UIImageView(image: UIImage(named: "advent_button"))
let dimension = 25 + drand48() * 30
imageView.frame = CGRect(x: 0, y: 0, width: dimension, height: dimension)
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleImageTap))
imageView.addGestureRecognizer(tapGestureRecognizer)
imageView.isUserInteractionEnabled = true
let animation = CAKeyframeAnimation(keyPath: "position")
animation.path = customPath().cgPath
animation.duration = 20
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
animation.repeatCount = 0.5
animation.timingFunction = CAMediaTimingFunction(name: .easeOut)
imageView.layer.add(animation, forKey: nil)
view.addSubview(imageView)
}
#objc func handleImageTap() {
print ("clicked")
}
func customPath() -> UIBezierPath {
let path = UIBezierPath()
path.move(to: CGPoint(x: 200, y: 0))
let endpoint = CGPoint(x: 20 , y: 700)
let cp1 = CGPoint(x: 90, y: 250)
let cp2 = CGPoint(x: 180, y: 400)
path.addCurve(to: endpoint, controlPoint1: cp1, controlPoint2: cp2)
return path
}
I've tried the following:
UIView.animate(withDuration: 30, delay: delay, options: [.allowUserInteraction], animations: {
self.button.frame = CGRect(
x: randomXEndShift,
y: 700,
width: dimension,
height: dimension)
}, completion: nil)
Shows the same behavoir. Even using .allowUserInteraction does not help at all.
I got your issue. i am sure you are touching layer not UIImageView and you have added uitapgesturerecognizer to UIImageview not layer. Do one thing set
animation.isRemovedOnCompletion = true
and set imageView.frame similar co- ordinates where animation will stop
Full code :
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let imageView = UIImageView(image: UIImage(named: "Icon123"))
imageView.backgroundColor = .red
// let dimension = 25 + drand48() * 30
imageView.isUserInteractionEnabled = true
imageView.frame = CGRect(x: 10, y: 200, width: 50, height: 50)
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleImageTap))
imageView.addGestureRecognizer(tapGestureRecognizer)
let animation = CAKeyframeAnimation(keyPath: "position")
animation.path = customPath().cgPath
animation.duration = 5
animation.fillMode = .forwards
animation.isRemovedOnCompletion = true
animation.repeatCount = 0.5
animation.timingFunction = CAMediaTimingFunction(name: .easeOut)
imageView.layer.add(animation, forKey: nil)
view.addSubview(imageView)
// Do any additional setup after loading the view, typically from a nib.
}
#objc func handleImageTap() {
print ("clicked")
}
func customPath() -> UIBezierPath {
let path = UIBezierPath()
path.move(to: CGPoint(x: 200, y: 0))
let endpoint = CGPoint(x: 20 , y: 700)
let cp1 = CGPoint(x: 90, y: 250)
let cp2 = CGPoint(x: 180, y: 400)
path.addCurve(to: endpoint, controlPoint1: cp1, controlPoint2: cp2)
return path
}
}

How do I flip over a UIBezierPath or cgPath thats animated onto the CAShapeLayer?

I have an array of type [UIBezierPath] that I transform into cgPaths and then animate onto a CAShapeLayer I called shapeLayer. Now for some reason all my paths are upside down, so all the paths get drawn upside down. How can I fix this, I tried a couple of methods but sadly none of them worked... I did however figure out how to scale the path. This is my code to draw the swiftPath, which is a path made up of UIBezierPaths found in the Forms class under the function swiftBirdForm(). Drawing the path is working fine, I just can't figure out how to flip it 180 degrees.
#objc func drawForm() {
var swiftPath = Forms.swiftBirdForm()
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.lineWidth = 1
shapeLayer.frame = CGRect(x: -120, y: 120, width: 350, height: 350)
var paths: [UIBezierPath] = swiftPath
guard let path = paths.first else {
return
}
paths.dropFirst()
.forEach {
path.append($0)
}
shapeLayer.transform = CATransform3DMakeScale(0.6, 0.6, 0)
shapeLayer.path = path.cgPath
self.view.layer.addSublayer(shapeLayer)
let strokeEndAnimation = CABasicAnimation(keyPath: "strokeEnd")
strokeEndAnimation.duration = 1.0
strokeEndAnimation.fromValue = 0.0
strokeEndAnimation.toValue = 1.0
shapeLayer.add(strokeEndAnimation, forKey: nil)
}
Using CATransform3D
shapeLayer.transform = CATransform3DMakeScale(1, -1, 1)
Transforming path,
let shapeBounds = shapeLayer.bounds
let mirror = CGAffineTransform(scaleX: 1,
y: -1)
let translate = CGAffineTransform(translationX: 0,
y: shapeBounds.size.height)
let concatenated = mirror.concatenating(translate)
bezierPath.apply(concatenated)
shapeLayer.path = bezierPath.cgPath
Transforming layer,
let shapeFrame = CGRect(x: -120, y: 120, width: 350, height: 350)
let mirrorUpsideDown = CGAffineTransform(scaleX: 1,
y: -1)
shapeLayer.setAffineTransform(mirrorUpsideDown)
shapeLayer.frame = shapeFrame

Custom Segmented Control Effect

There is what I want to do :
However, with Segmented Control : The only I can do is to choose background and tintColor.
How can I personalize Segmented Control to achieve this goal?
If not possible, How could I do ?
Many Thanks!
Try this in Swift 3:
mySegment.isMomentary = true
mySegment.layer.cornerRadius = mySegment.bounds.height / 2
mySegment.layer.borderColor = UIColor.black.cgColor
mySegment.layer.borderWidth = 1
mySegment.tintColor = UIColor.black
mySegment.clipsToBounds = true
Or you must to use UIBezierPath and CAShapeLayer
var OldShape = CAShapeLayer()
var OldShape2 = CAShapeLayer()
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
mySegment.isMomentary = true
mySegment.tintColor = UIColor.red
mySegment.setTitle("Oui", forSegmentAt: 0)
mySegment.setTitle("Non", forSegmentAt: 1)
drawBorderLine(myView: mySegment)
mySegment.layer.cornerRadius = mySegment.bounds.height / 2
mySegment.clipsToBounds = true
}
func drawBorderLine(myView : UISegmentedControl) {
let path = UIBezierPath(roundedRect: myView.bounds,
byRoundingCorners: [.topLeft, .bottomLeft, .bottomRight, .topRight],
cornerRadii: CGSize(width: 20.0, height: myView.bounds.height / 2))
path.move(to: CGPoint(x: myView.bounds.midX, y: myView.bounds.minY))
path.addLine(to: CGPoint(x: myView.bounds.midX, y: myView.bounds.maxY))
let shape = CAShapeLayer()
shape.path = path.cgPath
shape.fillColor = UIColor.clear.cgColor
shape.lineWidth = 3
shape.lineCap = kCALineCapRound
shape.strokeColor = UIColor.lightGray.cgColor
myView.layer.insertSublayer(shape, at: 0)
}
#IBAction func valueChange(_ sender: UISegmentedControl) {
// setting shape red
let shape = CAShapeLayer()
shape.fillColor = UIColor.clear.cgColor
shape.lineWidth = 3
shape.lineCap = kCALineCapRound
shape.strokeColor = UIColor.red.cgColor
// setting shape lightGray
let shape2 = CAShapeLayer()
shape2.fillColor = UIColor.clear.cgColor
shape2.lineWidth = 3
shape2.lineCap = kCALineCapRound
shape2.strokeColor = UIColor.lightGray.cgColor
switch sender.selectedSegmentIndex {
case 1:
let element = sender.subviews[0]
let path = UIBezierPath(roundedRect: element.bounds, byRoundingCorners: [.topRight, .bottomRight],
cornerRadii: CGSize(width: 20.0, height: element.bounds.height / 2))
shape.path = path.cgPath
let element2 = sender.subviews[1]
let path2 = UIBezierPath(roundedRect: element2.bounds, byRoundingCorners: [.bottomLeft, .topLeft],
cornerRadii: CGSize(width: 20.0, height: element2.bounds.height / 2))
shape2.path = path2.cgPath
SelectAndDeselect(myViewSelect: element, myViewDeselect: element2, select: shape, deselect: shape2)
case 0:
let element = sender.subviews[1]
let path = UIBezierPath(roundedRect: element.bounds, byRoundingCorners: [.topLeft, .bottomLeft],
cornerRadii: CGSize(width: 20.0, height: element.bounds.height / 2))
shape.path = path.cgPath
let element2 = sender.subviews[0]
let path2 = UIBezierPath(roundedRect: element2.bounds, byRoundingCorners: [.bottomRight, .topRight],
cornerRadii: CGSize(width: 20.0, height: element2.bounds.height / 2))
shape2.path = path2.cgPath
SelectAndDeselect(myViewSelect: element, myViewDeselect: element2, select: shape, deselect: shape2)
default:
break
}
}
func SelectAndDeselect(myViewSelect : UIView, myViewDeselect : UIView, select : CAShapeLayer, deselect : CAShapeLayer ) {
if OldShape.path != nil {
OldShape.removeFromSuperlayer()
OldShape = select
myViewSelect.layer.addSublayer(OldShape)
}else{
OldShape = select
myViewSelect.layer.addSublayer(OldShape)
}
if OldShape2.path != nil{
OldShape2.removeFromSuperlayer()
OldShape2 = deselect
myViewDeselect.layer.addSublayer(OldShape2)
}else{
OldShape2 = deselect
myViewDeselect.layer.addSublayer(OldShape2)
}
}