How to launch gradient animation on button tapped? - swift

I'm creating an IOS application. I have 5 buttons in star shapes created with UIBezierPath. And I want fill them when I tapped specific star. For ex. if I tapped 3-rd star then gradient will fill 3 first stars etc.
I have already created view with 5 stars. In the draw view method I have added code for animating gradient and it works fine only when view is launched.
override func draw(_ rect: CGRect) {
let path = UIBezierPath()
UIColor.black.setStroke()
path.lineWidth = 2
let numberOfPoints = 5
var angle: CGFloat = CGFloat.pi / 2
let angleOfIncrement = CGFloat.pi*2/CGFloat(numberOfPoints)
let radius: CGFloat = rect.origin.x + rect.width/2
let offset = CGPoint(x: rect.origin.x + rect.width/2, y: rect.origin.y + rect.height/2)
let midleRadius = radius*0.45
var firstStar = true
for _ in 1...numberOfPoints {
let firstPoint = getPointLocation(angle: angle, radius: midleRadius, offset: offset)
let midlePoint = getPointLocation(angle: angle+angleOfIncrement/2, radius: radius, offset: offset)
let secondPoint = getPointLocation(angle: angle+angleOfIncrement, radius: midleRadius, offset: offset)
if firstStar {
firstStar = false
path.move(to: firstPoint)
path.addLine(to: midlePoint)
path.addLine(to: secondPoint)
}else {
path.addLine(to: firstPoint)
path.addLine(to: midlePoint)
path.addLine(to: secondPoint)
}
angle += angleOfIncrement
}
path.stroke()
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
let gradient = CAGradientLayer(layer: layer)
gradient.colors = [UIColor.black.cgColor, UIColor.white.cgColor]
gradient.locations = [1]
gradient.startPoint = CGPoint(x: 0, y: 1)
gradient.endPoint = CGPoint(x: 1, y: 1)
gradient.frame = path.bounds
self.layer.addSublayer(gradient)
let animation = CABasicAnimation(keyPath: "locations")
animation.fromValue = [0, 0]
animation.toValue = [0, 1.0]
animation.duration = 3
gradient.add(animation, forKey: nil)
self.layer.mask = shapeLayer
}
I expect that when I tapped 3-rd star then gradient will fill first 3 stars.

You have to separate your drawing code from your animation code, and (btw.) you should also (re-)move the layer creation out of draw().
For the separation move
let animation = CABasicAnimation(keyPath: "locations")
animation.fromValue = [0, 0]
animation.toValue = [0, 1.0]
animation.duration = 3
gradient.add(animation, forKey: nil)
into a separate method and call it when needed.
You must also remove the layer creation from draw()because this method is always called if the view needs to be (re-)drawn. This produce a serious memory leak, because for instance every rotation of your iPhone will create a new gradient layer.
You have another issue in your code. Your gradient layer lies over the layer of the view. That cannot work. You may make the gradient layer to the view's layer, and work with mask, to cut the stars out of the gradient.

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)

Custom shape to UISlider and update progress in Swift 5

I have checked some of the StackOverflow answers regarding custom UIView slider but using them I unable to make the slider like this. This makes a circle or half circle. I have figured out some library that makes circle slider using UIView but its not helpful to me so could anyone please help me out. How can I make slider like in below UIImage? Thanks!
You will probably just roll your own. (You obviously could search for third party implementations, but that would be out of scope for StackOverflow.) There are a lot of ways of tackling this, but the basic elements here are:
The pink arc for the overall path. Personally, I'd use a CAShapeLayer for that.
The white arc from the start to the current progress (measured from 0 to 1). Again, a CAShapeLayer would be logical.
The white dot placed at the spot of the current progress. Below I create a CALayer with white background and then apply a CAGradientLayer as a mask to that. You could also just create a UIImage for this.
In terms of how to set the progress, you would set the paths of the pink and white arcs to the same path, but just update the strokeEnd of the white arc. You would also adjust the position of the white dot layer accordingly.
The only complicated thing here is figuring out the center of the arc. In my example below, I calculate it with some trigonometry based upon the bounds of the view so that arc goes from lower left corner, to the top, and back down the the lower right corner. Or you might instead pass the center of the arc as a parameter.
Anyway, it might look like:
#IBDesignable
class ArcView: UIView {
#IBInspectable
var lineWidth: CGFloat = 7 { didSet { updatePaths() } }
#IBInspectable
var progress: CGFloat = 0 { didSet { updatePaths() } }
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
progress = 0.35
}
lazy var currentPositionDotLayer: CALayer = {
let layer = CALayer()
layer.backgroundColor = UIColor.white.cgColor
let rect = CGRect(x: 0, y: 0, width: lineWidth * 3, height: lineWidth * 3)
layer.frame = rect
let gradientLayer = CAGradientLayer()
gradientLayer.colors = [UIColor.white.cgColor, UIColor.clear.cgColor]
gradientLayer.type = .radial
gradientLayer.frame = rect
gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 1, y: 1)
gradientLayer.locations = [0.5, 1]
layer.mask = gradientLayer
return layer
}()
lazy var progressShapeLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.lineCap = .round
shapeLayer.lineWidth = lineWidth
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
return shapeLayer
}()
lazy var totalShapeLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.lineCap = .round
shapeLayer.lineWidth = lineWidth
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = #colorLiteral(red: 0.9439327121, green: 0.5454334617, blue: 0.6426400542, alpha: 1)
return shapeLayer
}()
override init(frame: CGRect) {
super.init(frame: frame)
configure()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
configure()
}
override func layoutSubviews() {
super.layoutSubviews()
updatePaths()
}
}
// MARK: - Private utility methods
private extension ArcView {
func configure() {
layer.addSublayer(totalShapeLayer)
layer.addSublayer(progressShapeLayer)
layer.addSublayer(currentPositionDotLayer)
}
func updatePaths() {
let rect = bounds.insetBy(dx: lineWidth / 2, dy: lineWidth / 2)
let halfWidth = rect.width / 2
let height = rect.height
let theta = atan(halfWidth / height)
let radius = sqrt(halfWidth * halfWidth + height * height) / 2 / cos(theta)
let center = CGPoint(x: rect.midX, y: rect.minY + radius)
let delta = (.pi / 2 - theta) * 2
let startAngle = .pi * 3 / 2 - delta
let endAngle = .pi * 3 / 2 + delta
let path = UIBezierPath(arcCenter: center,
radius: radius,
startAngle: startAngle,
endAngle: endAngle,
clockwise: true)
progressShapeLayer.path = path.cgPath // white arc
totalShapeLayer.path = path.cgPath // pink arc
progressShapeLayer.strokeEnd = progress
let currentAngle = (endAngle - startAngle) * progress + startAngle
let dotCenter = CGPoint(x: center.x + radius * cos(currentAngle),
y: center.y + radius * sin(currentAngle))
currentPositionDotLayer.position = dotCenter
}
}
Above, I set the background color of the ArcView so you could see its bounds, but you would obviously set the background color to be transparent.
Now there are tons of additional features you might add (e.g. add user interaction so you could “scrub” it, etc.). See https://github.com/robertmryan/ArcView for example. But the key when designing this sort of stuff is to just break it down into its constituent elements, layering one on top of the other.
You can programmatically set the progress of the arcView to get it to change the current value between values of 0 and 1:
func startUpdating() {
arcView.progress = 0
var current = 0
Timer.scheduledTimer(withTimeInterval: 0.2, repeats: true) { [weak self] timer in
current += 1
guard let self = self, current <= 10 else {
timer.invalidate()
return
}
self.arcView.progress = CGFloat(current) / 10
}
}
Resulting in:

Using UIViewPropertyAnimator to drive animation along curved path?

I have an interruptible transition that is driven by UIViewPropertyAnimator and I would like to animate a view along a non linear path to its destination.
Doing some research I've found that the way to do this is by using:
CAKeyframeAnimation. However, I'm having issue coordinating the animations also I'm having issue with the CAKeyframeAnimation fill mode.
It appears to be reverse even though I've set it to forwards.
Below is the bezier path:
let bezierPath = self.generateCurve(from: CGPoint(x: self.itemStartFrame.midX, y: self.itemStartFrame.midY), to: CGPoint(x: self.itemEndFrame.midX, y: self.itemEndFrame.midY), bendFactor: 1, thickness: 1)
func generateCurve(from: CGPoint, to: CGPoint, bendFactor: CGFloat, thickness: CGFloat) -> UIBezierPath {
let center = CGPoint(x: (from.x+to.x)*0.5, y: (from.y+to.y)*0.5)
let normal = CGPoint(x: -(from.y-to.y), y: (from.x-to.x))
let normalNormalized: CGPoint = {
let normalSize = sqrt(normal.x*normal.x + normal.y*normal.y)
guard normalSize > 0.0 else { return .zero }
return CGPoint(x: normal.x/normalSize, y: normal.y/normalSize)
}()
let path = UIBezierPath()
path.move(to: from)
let midControlPoint: CGPoint = CGPoint(x: center.x + normal.x*bendFactor, y: center.y + normal.y*bendFactor)
let closeControlPoint: CGPoint = CGPoint(x: midControlPoint.x + normalNormalized.x*thickness*0.5, y: midControlPoint.y + normalNormalized.y*thickness*0.5)
let farControlPoint: CGPoint = CGPoint(x: midControlPoint.x - normalNormalized.x*thickness*0.5, y: midControlPoint.y - normalNormalized.y*thickness*0.5)
path.addQuadCurve(to: to, controlPoint: closeControlPoint)
path.addQuadCurve(to: from, controlPoint: farControlPoint)
path.close()
return path
}
Path visualization:
let shapeLayer = CAShapeLayer()
shapeLayer.path = bezierPath.cgPath
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = 2
containerView.layer.addSublayer(shapeLayer)
Here's the CAKeyframeAnimation that resides inside of the UIViewPropertyAnimator
let curvedPathAnimation = CAKeyframeAnimation(keyPath: "position")
curvedPathAnimation.path = bezierPath.cgPath
curvedPathAnimation.fillMode = kCAFillModeForwards
curvedPathAnimation.calculationMode = kCAAnimationPaced
curvedPathAnimation.duration = animationDuration
self.attatchmentView.layer.add(curvedPathAnimation, forKey: nil)
So far this animation along the correct path, however the animation reverse back to it's starting position and is not in sync with the property animator.
Any suggestions?

Swift: Animate position of a path on a CAShapeLayer

I have a path and a shape Layer on that I want to draw a line
let path = UIBezierPath()
path.move(to: CGPoint(x: xValue,y: yValue))
path.addLine(to: CGPoint(x: xValue, y: yValue + 300))
// create shape layer for that path
let shapeLayer = CAShapeLayer()
shapeLayer.strokeColor = #colorLiteral(red: 1, green: 0, blue: 0, alpha: 1).cgColor
shapeLayer.lineWidth = 3
shapeLayer.path = path.cgPath
But I am also animating the drawing of the line
self.chartView.layer.addSublayer(shapeLayer)
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.duration = 0.6
shapeLayer.add(animation, forKey: "MyAnimation")
Now to my question:
I also want to animate the position of my path at the meanwhile. So at the moment my line is on the x-Positon of "xValue". I want to move it animated to "xValue - 40.0". I tried it on 2 ways which both didnt work.
use UIView.animate. Problem: The x Position is changing but without an animation:
UIView.animate(withDuration: 3.0) {
var highlightFrame = shapeLayer.frame
highlightFrame.origin.x = (highlightFrame.origin.x) - 40
shapeLayer.frame = highlightFrame
}
add a animation on the shape layer. Nothing happens here
let toPoint:CGPoint = CGPoint(x: -40.0, y: 0.0)
let fromPoint:CGPoint = CGPoint.zero
let movement = CABasicAnimation(keyPath: "movement")
movement.isAdditive = true
movement.fromValue = NSValue(cgPoint: fromPoint)
movement.toValue = NSValue(cgPoint: toPoint)
movement.duration = 0.3
shapeLayer.add(movement, forKey: "move")
Anyone has another solution? Thanks for your help.
let toPoint:CGPoint = CGPoint(x: -40.0, y: 0.0)
let fromPoint:CGPoint = CGPoint.zero
let movement = CABasicAnimation(keyPath: "position") // it should be "position" not "movement"
movement.isAdditive = true
movement.fromValue = NSValue(cgPoint: fromPoint)
movement.toValue = NSValue(cgPoint: toPoint)
movement.duration = 0.3
shapeLayer.add(movement, forKey: "move")

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