Trying to restart UIBezierPath on button click - ios

I'm trying to restart a UIBezierPath with the click of a button.
I tried to play around with the opacity.
Basically, I have 3 arrows that get drawn in an animation, and then if the user clicks on the animate button, I want them to re-animate.
This is what I have for drawing the arrows:
func animateArrow(firstPoint: CGPoint, endPoint:CGPoint) {
let arrow = UIBezierPath.bezierPathWithArrowFromPoint(firstPoint, endPoint: endPoint, tailWidth: 7.5, headWidth: 15, headLength: 15)
let arrowPath = CAShapeLayer()
arrowPath.path = arrow.CGPath
arrowPath.fillColor = UIColor.whiteColor().CGColor
arrowPath.strokeColor = UIColor.blackColor().CGColor
let opacityAnim = CABasicAnimation()
opacityAnim.keyPath = "opacity"
opacityAnim.fromValue = NSNumber(float: 0.0)
opacityAnim.toValue = NSNumber(float: 0.7)
opacityAnim.duration = 1.0
arrowPath.addAnimation(opacityAnim, forKey: "opacity")
arrowPath.opacity = 0.7
self.view.layer.addSublayer(arrowPath)
}
This is the animation part:
dispatch_after(time, dispatch_get_main_queue(), {
self.colorize(labelArray[b.toInt()!], color:self.cyan)
self.animateArrow(pointArray[a.toInt()!], endPoint: pointArray[b.toInt()!])
})
dispatch_after(time2, dispatch_get_main_queue(), {
self.colorize(labelArray[c.toInt()!], color:self.cyan)
self.animateArrow(pointArray[b.toInt()!], endPoint: pointArray[c.toInt()!])
})
dispatch_after(time3, dispatch_get_main_queue(), {
self.colorize(labelArray[d.toInt()!], color:self.red)
self.animateArrow(pointArray[c.toInt()!], endPoint: pointArray[d.toInt()!])
self.animateButton.enabled = true
})

You can clear all the sublayers you have added by removing the sublayer from view inside button click event.
Please make sure that you draw the paths in a separate overlay view and not the underlying view that contains the button and other UI. If you use the code below in the main view, the UIView layers will also get removed.
func clicked (button:UIButton) {
for layer in viewToClear.layer.sublayers as [CALayer]
{
layer.removeFromSuperlayer()
}
}

Related

Swift image appear animation from bottom of its frame to top (wipe animation)

I just want to get some ideas on how to approach the situation I have in hand.
I have created some images which I want to animate, but the animation I want is not default given, so its giving me some hard time.
The animation I want is like this: https://i.stack.imgur.com/byGMC.gif (not from bottom of the screen, from the bottom of images' own frame)
When a button is pressed I want the button/image start appear from bottom of its frame to the top, like the wipe animation in powerpoint :)
So naturaly it will default as hidden and when a button is pressed it will animate in.
I did try a few methods but non of them is actually doing the job I want.
Like this one:
extension UIView{
func animShow(){
UIView.animate(withDuration: 2, delay: 5, options: [.transitionFlipFromBottom],
animations: {
self.center.y -= self.bounds.height
self.layoutIfNeeded()
}, completion: nil)
self.isHidden = false
}
func animHide(){
UIView.animate(withDuration: 1, delay: 0, options: [.curveLinear],
animations: {
self.center.y += self.bounds.height
self.layoutIfNeeded()
}, completion: {(_ completed: Bool) -> Void in
self.isHidden = true
})
}
}
so it slides in the image but this is not what I want, you may try the code, just add this and write image.animShow() in viewDidLoad, button is the button which I want to animate.
I appreciate every bit of help, and I am a newbie in swift programming
Thank you.
There are various ways to do a "soft wipe" animation... here is one.
We can add a gradient layer mask and then animate the gradient.
When using a layer mask, clear (alpha 0) areas hide what's under the mask, and opaque (alpha 1) areas show what's under the mask. This includes parts of the mask that are translucent - alpha 0.5 for example - so the content becomes "translucent".
To get a "soft wipe" we want the gradient to use only a portion of the frame, so if we set the starting Y point to 0.5 (halfway down), for example, we can set the ending Y point to 0.6 ... this would give us a horizontal "gradient band".
So, a yellow-to-red gradient at 0.5 to 0.6 would look like this:
If we set the starting Y to 1.0 and the ending Y to 1.1, the gradient band will start "off the bottom of the view" ... and we can animate it up until it's "off the top of the view" (note that converted to gif loses some of the smooth gradient property):
Now, if we instead use clear-to-red and set it as a layer mask, using the same animation it will "soft wipe" the view, it will look something like this:
Can't get the animated gif small enough to post here, so here's a link to the animation: https://i.imgur.com/jo2DH3Z.mp4
Here's some sample code for you to play with. Lots of comments in there:
class AnimatedGradientImageView: UIImageView {
let maskLayer = CAGradientLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// vertical gradient
// start at bottom
maskLayer.startPoint = CGPoint(x: 0.5, y: 1.0)
// to bottom + 1/10th
maskLayer.endPoint = CGPoint(x: 0.5, y: 1.1)
// use "if true" to see the layer itself
// use "if false" to see the image reveal
if false {
// yellow to red
maskLayer.colors = [UIColor.yellow.cgColor, UIColor.red.cgColor]
// add it as a sublayer
layer.addSublayer(maskLayer)
} else {
// clear to red
maskLayer.colors = [UIColor.clear.cgColor, UIColor.red.cgColor]
// set the mask
layer.mask = maskLayer
}
}
override func layoutSubviews() {
super.layoutSubviews()
// set mask layer frame
maskLayer.frame = bounds
}
func reveal() -> Void {
let anim1: CABasicAnimation = CABasicAnimation(keyPath: "startPoint.y")
// anim1 animates the gradient start point Y
// to -0.1 (1/10th above the top of the view)
anim1.toValue = -0.1
anim1.duration = 1.0
anim1.isRemovedOnCompletion = false
anim1.fillMode = .forwards
let anim2: CABasicAnimation = CABasicAnimation(keyPath: "endPoint.y")
// anim2 animates the gradient end point Y
// to 0.0 (the top of the view)
anim2.toValue = 0.0
anim2.duration = 1.0
anim2.isRemovedOnCompletion = false
anim2.fillMode = .forwards
maskLayer.add(anim1, forKey: nil)
maskLayer.add(anim2, forKey: nil)
}
}
class AnimatedGradientViewController: UIViewController {
let testImageView = AnimatedGradientImageView(frame: .zero)
override func viewDidLoad() {
super.viewDidLoad()
// replace with your image name
guard let img = UIImage(named: "sampleImage") else {
fatalError("Could not load image!!!")
}
// set the image
testImageView.image = img
testImageView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(testImageView)
// respect safe area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// size: 240 x 240
testImageView.widthAnchor.constraint(equalToConstant: 240.0),
testImageView.heightAnchor.constraint(equalToConstant: 240.0),
// centered
testImageView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
testImageView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
// tap anywhere in the view
let t = UITapGestureRecognizer(target: self, action: #selector(gotTap(_:)))
view.addGestureRecognizer(t)
}
#objc func gotTap(_ g: UITapGestureRecognizer) -> Void {
testImageView.reveal()
}
}
First, you will have to give the height constraint of the image from the storyboard and set the height to 0
and take the image height constraint into your file like below
#IBOutlet weak var imageHeight: NSLayoutConstraint!
Here is the animation code.
UIView.animate(withDuration: 2, delay: 2, options: [.transitionFlipFromBottom],
animations: {
self.image.frame = CGRect(x: self.image.frame.origin.x, y: self.image.frame.origin.y - 100, width: self.image.frame.size.width, height: 100)
self.imageHeight.constant = 100
self.image.layoutIfNeeded()
}, completion: nil)
Click here to see animation

Synchronously animate CALayer property and UIView with UIViewPropertyAnimator

I'm using a UIViewPropertyAnimator to animate the position of a view. But, I also want to animate CALayer properties like borderColor along with it. Currently, it doesn't animate and instantly changes at the start, like this:
Here's my code:
class ViewController: UIViewController {
var animator: UIViewPropertyAnimator?
let squareView = UIView(frame: CGRect(x: 0, y: 40, width: 80, height: 80))
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(squareView)
squareView.backgroundColor = UIColor.blue
squareView.layer.borderColor = UIColor.green.cgColor
squareView.layer.borderWidth = 6
animator = UIViewPropertyAnimator(duration: 2, curve: .easeOut, animations: {
self.squareView.frame.origin.x = 100
self.squareView.layer.borderColor = UIColor.red.cgColor
})
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.animator?.startAnimation()
}
}
}
I looked at this question, How to synchronously animate a UIView and a CALayer, and the answer suggested using "an explicit animation, like a basic animation." It said,
If the timing function and the duration of both animations are the same then they should stay aligned.
However, if I use CABasicAnimation, I lose all the benefits of UIViewPropertyAnimator, like timing and stopping in the middle. I will also need to keep track of both. Is there any way to animate CALayer properties with UIViewPropertyAnimator?
CABasicAnimation has timing as well as keyframe animation to stop in the middle. But, to replicate your animation above:
squareView.layer.transform = CATransform3DMakeTranslation(100, 0, 0)
let fromValue = squareView.transform
let toValue = 100
let translateAnimation = CABasicAnimation(keyPath: #keyPath(CALayer.transform))
translateAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
translateAnimation.fromValue = fromValue
translateAnimation.toValue = toValue
translateAnimation.valueFunction = CAValueFunction(name: .translateX)
let fromColor = squareView.layer.borderColor
let toColor = UIColor.red.cgColor
let borderColorAnimation = CABasicAnimation(keyPath: "borderColor")
borderColorAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
borderColorAnimation.fromValue = fromColor
borderColorAnimation.toValue = toColor
let groupAnimation = CAAnimationGroup()
groupAnimation.animations = [translateAnimation, borderColorAnimation]
groupAnimation.duration = 5
squareView.layer.add(groupAnimation, forKey: nil)

Animate CAShapeLayer path with CASpringAnimation

I have a custom UIView where I add a CAShapeLayer as a direct sublayer of the view's layer:
private let arcShapeLayer = CAShapeLayer()
The CAShapeLayer has the following properties (the border is to visualize more easily the animation) declared in the awakeFromNib:
arcShapeLayer.lineWidth = 2.0
arcShapeLayer.strokeColor = UIColor.black.cgColor
arcShapeLayer.fillColor = UIColor.lightGray.cgColor
arcShapeLayer.masksToBounds = false
layer.masksToBounds = false
layer.addSublayer(arcShapeLayer)
I declare the frame of the arcShapeLayer in the layoutSubviews:
arcShapeLayer.frame = layer.bounds
Then I have the following function where I pass a certain percentage (from the ViewController) in order to achieve the arc effect after some dragging. Simply put I add a quadCurve at the bottom of the layer:
func configureArcPath(curvePercentage: CGFloat) -> CGPath {
let path = UIBezierPath()
path.move(to: CGPoint.zero)
path.addLine(to:
CGPoint(
x: arcShapeLayer.bounds.minX,
y: arcShapeLayer.bounds.maxY
)
)
path.addQuadCurve(
to:
CGPoint(
x: arcShapeLayer.bounds.maxX,
y: arcShapeLayer.bounds.maxY
),
controlPoint:
CGPoint(
x: arcShapeLayer.bounds.midX,
y: arcShapeLayer.bounds.maxY + arcShapeLayer.bounds.maxY * (curvePercentage * 0.4)
)
)
path.addLine(to:
CGPoint(
x: arcShapeLayer.bounds.maxX,
y: arcShapeLayer.bounds.minY
)
)
path.close()
return path.cgPath
}
Then I try to animate with a spring effect the arc with the following code:
func animateArcPath() {
let springAnimation = CASpringAnimation(keyPath: "path")
springAnimation.initialVelocity = 10
springAnimation.mass = 10
springAnimation.duration = springAnimation.settlingDuration
springAnimation.fromValue = arcShapeLayer.path
springAnimation.toValue = configureArcPath(curvePercentage: 0.0)
springAnimation.fillMode = .both
arcShapeLayer.add(springAnimation, forKey: nil)
arcShapeLayer.path = configureArcPath(curvePercentage: 0.0)
}
The problem, as you can see in the video, is that the arc never overshoots. Although it oscillates between its original position and the rest position, the spring effect is never achieved.
What am I missing here?
I played around with this and I can report that you are absolutely right. It has nothing to do clamping at zero. The visible animation clamps at both extremes.
Let's say you supply a big mass value so that the overshoot should go way past the initial position of the convex bow on its return journey back to the start. Then what we see is that the animation clamps both at that original position and at the minimum you are trying to animate to. As the spring comes swinging between them, we see the animation happening between one extreme and the other, but as it reaches the max or min it just clamps there until it has had time to swing to its full extent and return.
I have to conclude that CAShapeLayer path doesn't like springing animations. This same point is raised at CAShapeLayer path spring animation not 'overshooting'
I was able to simulate the sort of look you're probably after by chaining normal basic animations:
Here's the code I used (based on your own code):
#IBAction func animateArcPath() {
self.animate(to:-1, in:0.5, from:arcShapeLayer.path!)
}
func animate(to arc:CGFloat, in time:Double, from current:CGPath) {
let goal = configureArcPath(curvePercentage: arc)
let anim = CABasicAnimation(keyPath: "path")
anim.duration = time
anim.fromValue = current
anim.toValue = goal
anim.delegate = self
anim.setValue(arc, forKey: "arc")
anim.setValue(time, forKey: "time")
anim.setValue(goal, forKey: "pathh")
arcShapeLayer.add(anim, forKey: nil)
}
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
if let arc = anim.value(forKey:"arc") as? CGFloat,
let time = anim.value(forKey:"time") as? Double,
let p = anim.value(forKey:"pathh") {
if time < 0.05 {
return
}
self.animate(to:arc*(-0.5), in:time*0.5, from:p as! CGPath)
}
}

Custom animation for loading data

Here I have been trying so long to create a custom animated progress bar.
I'm trying to move a view from screen's one end to second end with resizing.
view's frmae is (0, 100, 10, 4)
What I want is...
At start of animation view should be at its initial position, lets say at (0, 100)
in the middle of the animation view will be at the middle of the screen and its width will be increased to 100px.
at the end of the animation view will be at screen's second end. and frame of the view will be at, lets say (400, 100) and width off the view will be again back to 10px
The whole progress should be reversed also.
UIButton Action method
var mainView = UIView()
#IBAction func btnTapped(_ sender: UIButton) {
let rect = CGRect(x: 0, y: 300, width: 10, height: 4)
mainView = UIView(frame: rect)
mainView.backgroundColor = UIColor.black
self.view.addSubview(mainView)
animate()
}
Animation Method
func animate() {
let theAnimationTranslationX = CABasicAnimation(keyPath: "transform.translation.x")
theAnimationTranslationX.fromValue = mainView.bounds.minX
theAnimationTranslationX.toValue = 400
let scaleAnimate:CABasicAnimation = CABasicAnimation(keyPath: "transform.scale.x")
scaleAnimate.fromValue = NSValue(caTransform3D: CATransform3DMakeScale(1, 1, 1))
scaleAnimate.toValue = NSValue(caTransform3D: CATransform3DMakeScale(1.2, 0, 0))
let group = CAAnimationGroup()
group.autoreverses = true
group.repeatCount = .greatestFiniteMagnitude
group.animations = [theAnimationTranslationX]//, scaleAnimate]
group.duration = 2
group.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
mainView.layer.add(group, forKey: "animation")
}
I'm not getting the desired animation effect.
Is there any other way to do so or is it possible with any other way please suggest me.
Thanks for you time and any help will be appreciated.
EDIT:
The final animation should look like...

drawing circular progress view in iOS

Taking a lot from these questions here: Problems animating a countdown in swift sprite kit, and Animating circular progress View in Swift
However, the examples above (and all the other examples I've found online) show the progress view animating with a set duration of time. Mine is animating the download progress of a UIImage into a UITableView cell. Here is my code so far. In my UITableViewCell subclass, I have this:
func drawProgress(toValue: Float) {
let circlePath = UIBezierPath(arcCenter: CGPoint(x: self.contentView.frame.width / 2, y: self.contentView.frame.height / 2), radius: 30, startAngle: 0, endAngle: 6.28, clockwise: true)
// create its cooresponding layer
let circleLayer = CAShapeLayer()
circleLayer.frame = self.contentView.bounds
circleLayer.path = circlePath.CGPath
circleLayer.strokeColor = UIColor.blackColor().CGColor
circleLayer.fillColor = self.contentView.backgroundColor?.CGColor
circleLayer.lineWidth = 1.0
self.contentView.layer.addSublayer(circleLayer)
// create the animation
let pathAnimation = CABasicAnimation(keyPath: "strokeEnd")
pathAnimation.duration = 0.1
pathAnimation.fromValue = NSNumber(float: 0.0)
pathAnimation.toValue = NSNumber(float: toValue)
// apply the animation to path
circleLayer.addAnimation(pathAnimation, forKey: "strokeEnd")
}
I am using NSURLSessionDownloadDelegate to monitor the download progress of the NSData. Inside that method, I get all necessary information to track the download %. Here is my code there:
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
//...other code above to get the correct cell for index path, etc
myCell.drawProgress(toValue:download.progress)
//trackCell.progressView.progress = download.progress
}
The commented out line:
//myCell.progressView.progress = download.progress
is a UIProgressBar. This UIProgressBar reports and shows in the cell the correct progress every time, so I know that I have all the necessary info, and the info is correct, so it's just something about how I am implementing this to make it not work. Currently, when I load the tableView, I can see the first few cells draw out the animation really quickly (faster than the actual download), and then when I scroll down the tableView, all the progress bars are already full, and i don't see any animation in them.
I know that download.progress contains the correct float that is tracking the download process. How can I get this CAShapeLayer to correctly animate for each cell?
thanks
var progressCircle = CAShapeLayer()
// computed property - set() property observer updates circle when passed new progress value
var progress: CGFloat {
get {
return progressCircle.strokeEnd
}
set {
if (newValue >= 1) {
progressCircle.strokeEnd = 1
} else if (newValue < 0) {
progressCircle.strokeEnd = 0
} else {
progressCircle.strokeEnd = newValue
}
}
}
To Use - set up your progress circle object
func createProgressCircle() {
let rotationTransform = CATransform3DRotate(CATransform3DIdentity, CGFloat(-M_PI_2), 0, 0, 1.0)
// you can either create a rect for the circle now programmatically
//or pass in the frame of an existing storyboard object to draw within
let rectForCircle = CGRect(origin: CGPoint(myView.origin), size: CGSize(width: myView.width, height: myView.height))
progressCircle.path = circlePath(inRect: rectForCircle).cgPath
progressCircle.lineWidth = 4.0
progressCircle.fillColor = UIColor.clear.cgColor
progressCircle.strokeColor = sitchRed.cgColor
progressCircle.transform = rotationTransform
circleShapeView.layer.addSublayer(progressCircle)
circleShapeView.backgroundColor = UIColor.clear // make sure the circleView is clear so underlayer circle shape can show through
progress = 0.0
}
To show progress changes, pass the current progress value of your download into to progress property in your viewController, and the set() property observer will update the current stroke position of the circle :)

Resources