How to control the speed of UIView animation? - ios

I am implementing a rotating digit view like this:
https://github.com/jonathantribouharet/JTNumberScrollAnimatedView
But, instead of scrolling from say 1 to 9, I want the animation to continue scrolling (1 to 9 then back to 1 to 9), until I manually stop it.
I implement this by adding UILabel subviews 1-9, vertically stacked, and add an extra label for number 1 at the bottom. When rotating to the bottom (showing the bottom label 1), in the animation completion closure, I set the transform back to origin, showing top label 1, and restart rotating animation.
This works fine, but since each rotation (1-9-1) is separate, the animation from 1-9 accelerates and then stop at 1, and pick up speed at the next iteration. This is not what I want, I want the rotation to maintain a constant speed between the iterations.
How can I do this with UIView animation?
Here is the code:
public func startAnimation(){
UIView.setAnimationCurve(UIViewAnimationCurve.easeIn)
UIView.animate(withDuration: TimeInterval(self.loopDuration), animations: { [unowned self] in
self.container!.transform = CGAffineTransform(translationX: 0, y: CGFloat(-(self.endNumber-self.startNumber+1)*(self.size)))
}) { [unowned self] (completed) in
if self.isAnimationStopped {
self.container!.transform = CGAffineTransform(translationX: 0, y: 0)
UIView.setAnimationCurve(UIViewAnimationCurve.easeOut)
UIView.animate(withDuration: TimeInterval(self.loopDuration), animations: {
self.container!.transform = CGAffineTransform(translationX: 0, y: CGFloat(-self.targetY))
})
} else {
self.container!.transform = CGAffineTransform(translationX: 0, y: 0)
self.startAnimation()
}
}
}

By default UIView animation uses ease-in, ease-out animation timing, where the animation accelerates smoothly, runs, then decelerates to a stop.
You need to use a longer form of UIView.animate, animate(withDuration:delay:options:animations:completion:) that takes an options parameter, and specify a timing curve of .curveLinear.
Just specify 0.0 for the delay and options: .curveLinear
Edit:
Note that if you need to specify multiple options, like .curveLinear and .allowUserInteraction you would use OptionSet syntax, like options: [.curveLinear, .allowUserInteraction]

Related

UIView.animate with limited repeat and autoreverse glitches on a real device

I'm trying to make a simple scale animation of a button 1.0 -> 1.4, repeat 3 times and then stop, but I'm experiencing a glitch on a real device.
The code below works smoothly on a simulator, but on a real device (iPhone 12 Pro with iOS 16) it glitches and I can see the jump to scale 1.4 after animation is done, and then it jumps to scale 1.0 on completion where I set transform = .identity.
I've suspected UIView.modifyAnimations, but the same problem happens with the now deprecated UIView.setAnimationRepeatCount(3).
Here is the video of the glitch on the real device:
https://i.imgur.com/x1LJ9Ox.mp4
UIView.animate(withDuration: 0.5, delay: 0, animations: {
UIView.modifyAnimations(withRepeatCount: 3, autoreverses: true) {
self.titleButton.transform = CGAffineTransformMakeScale(1.4, 1.4)
// self.titleButton.layoutIfNeeded() // doesn't change anything
}
}, completion: { _ in
self.titleButton.transform = .identity
})
I've tested on my iphone 13 iOS 16, it glitches like you said above.
Maybe I'm wrong or misunderstand something but looks like something is changing in modifyAnimations(withRepeatCount:autoreverses:animations:).
In simulator, after the animation in modifyAnimations is done, it immediately go to completion. If in completion do nothing with the view, it will turn back to CGAffineTransformMakeScale(1.4, 1.4). But if the view has transform anything, it will turn to that transform.
But in real device, after the animation in modifyAnimations is done, the view will immediately turn back to the final of animation which here is CGAffineTransformMakeScale(1.4, 1.4) ( the simulator doesn't have this step) then after that go to completion. This cause the glitches like you said.
I have a workaround for this situation
UIView.animate(withDuration: 0.5, animations: {
// stop at the half of the final repeat means no autoreverses accur at the end
UIView.modifyAnimations(withRepeatCount: 2.5, autoreverses: true) {
self.secondView.transform = CGAffineTransformMakeScale(1.4, 1.4)
}
}, completion: { _ in
// make final scale back here
UIView.animate(withDuration: 0.5, animations: {
self.secondView.transform = .identity
})
})
It looks like iOS 16 is updating the transform 1 event loop after the animation resulting in the glitch. One way to work around this is to leave the scale at 1.0 at the end of the animation itself by using a keyframe animation (which I found at this answer) and adding the code to repeat 3 times:
UIView.animateKeyframes(withDuration: 1, delay: 0, options: [], animations: {
UIView.modifyAnimations(withRepeatCount: 3, autoreverses: false) {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.5) {
self.titleButton.transform = CGAffineTransformMakeScale(1.4, 1.4)
}
UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.5) {
self.titleButton.transform = .identity
}
}
}) { (isFinished) in
}

How to implement such pop over view animation?

I was using this ride-sharing ios app and found this pop over animation of options view. I wanted to implement similar pop over but could figure out if it is a custom transition or animation? Here is a link to the GIF of popover in application.
It will be helpful to link me to examples/tutorial/code with similar animation so that I can begin implementing on my ios app.
This is pretty simple. Just position your options view on the screen where you want it to appear and set the alpha to 0 so it is hidden. Then, prior to animation, make the view wider using scaleX, and translate it down using translationY. Then simply animate it back to .identity and animate the alpha back to 1.0 so it fades in. A basic example is below. When you dismiss the view you just do the opposite. Let me know if you need help with that.
yourView.transform = CGAffineTransform(scaleX: 1.3, y: 0)
yourView.transform = CGAffineTransform(translationX: 0.0, y: 200.0)
yourView.alpha = 0
UIView.animate(withDuration: 0.3, animations: {
yourView.transform = .identity
yourView.alpha = 1.0
})
I achieved the following animation using leading, trailing and button constraints. Please let me know if such animation can be done in other ways.
yourview.alpha = 0
UIView.animate(withDuration: 0.6, animations: {
self.leadingConstraints.constant = 20
self.trailConstraints.constant = -20
self.buttonConstraints.constant = 20
self.yourview.alpha = 1
self.yourview.layoutIfNeeded()
})

Using UIView.animateKeyframes to animate CAShapeLayer properties

I'm struggling to understand why the following key-framed animation doesn't perform as I expect. Here minuteClock is a subclassed UIView that is also a child view of the main view. minuteClock's horizontal position is set via an exposed constraint. minuteClock's own layer has a CAShapeLayer sublayer called borderLayer.
I'm trying to simultaneously animate the location of minuteClock and the fillcolor of that grandchild borderLayer. What I'm finding is that the keyframes work for the positioning but are ignored for the CAShapeLayer. That is, while the minuteClock moves to 100 half-way through the overall duration, I don't see borderLayer's fillcolor turn red. Instead it seems to just animates very quickly (like a quarter-second) to green.
UIView.animateKeyframes(withDuration: 2.0,
delay: 0,
options: .calculationModeLinear,
animations: {
UIView.addKeyframe(withRelativeStartTime: 0.0,
relativeDuration: 0.5,
animations: {
self.minuteClockPositionConstraint.constant = 100
self.minuteClock.borderLayer.fillColor = UIColor.red.cgColor
self.view.layoutIfNeeded()
})
UIView.addKeyframe(withRelativeStartTime: 0.5,
relativeDuration: 0.5,
animations: {
self.minuteClockPositionConstraint.constant = 50
self.minuteClock.borderLayer.fillColor = UIColor.green.cgColor
self.view.layoutIfNeeded()
})
}, completion: nil)
I think I'm probably going about this wrong, mixing up keyframed animation at the view level with implicit animation triggered by changing a property on a CAShapeLayer.
Any suggestions as to how I should be doing this?

UIView animation in completion block starts with displacement

I wrote a simple animation chain with two animations using UIView.animate, but the second animation in completion block starts not exactly from where the first animation ens so I have strange displacement to the right. Anyone could help? Maybe I did not fully understand tranfrorm property.
UIView.animate(withDuration: 3, animations: {
self.redView.transform = self.redView.transform.translatedBy(x: 100, y: 0)
}) { (_) in
UIView.animate(withDuration: 2, animations: {
self.redView.transform = self.redView.transform.scaledBy(x: 2, y: 2)
})
}
My redView should be moved to right on 100 and then from the same place became twice as large. But before second animation there is displacement to the right. I have no ideas about why this happens.
Thanks!
Gif with this issue:
Not sure what's your intention but I'd animate the frame in the first block:
let initialFrame = redView.frame
UIView.animate(withDuration: 3, animations: {
self.redView.frame = initialFrame.offsetBy(dx: 100, dy: 0)
}) { (_) in
UIView.animate(withDuration: 2, animations: {
self.redView.transform = self.redView.transform.scaledBy(x: 2, y: 2)
})
}

UIView animation with autolayout and child views

I have a weird problem that seems to be fairly easy to solve. I think I could use some workaround to be able to have the behavior that I want but I want to know if there is a better way to do it.
I have a view called contentView (blue in the image) that it will expand its height using a UIView.animation, this is no problem at all and works as expected.
The problem here is that this view has a child component (a button) that has an autolayout constraint to its bottom equal to 22 like this:
This is the autolayout constraint:
If I do the height resize without the animation it works fine, the view height change and the button are always 22 points of the bottom of the contentView. But when I use an animation to make this change more smoothy and user-friendly the button moves to the end position before the animation even start.
I want to know how I could achieve a smooth animation here but with the button moving along its parent view
The part of the code that handles the animation is pretty straightforward but I'll post it in here:
#IBAction func openDetail(_ sender: ExpandCourseDetail) {
let rotation = sender.getOpen() ? CGAffineTransform.identity : CGAffineTransform(rotationAngle: CGFloat.pi)
UIView.animate(withDuration: 0.5, delay: 0.1, options: [.curveEaseInOut], animations: {
sender.transform = rotation
}, completion: {
success in
sender.setOpen(!sender.getOpen())
})
UIView.animate(withDuration: 1.0, delay: 0.5, options: [.curveEaseInOut], animations: {
self.contentView.frame.size.height = sender.getOpen() ? self.contentView.frame.height - 300 : self.contentView.frame.height + 300
}, completion: nil)
}
As a side note, the button itself has an animation that rotates the button 180 degrees to show the user that the view is expanding.
Thank you so much for your help.
It's super easy with constraints, just create a superView height constraint IBOutlet and change its constant value.
#IBAction func btnPressed(_ sender: UIButton) {
self.toggleButton.isSelected = !sender.isSelected
//Animation starts here
self.view.layoutIfNeeded()
UIView.animate(withDuration: 0.7) {
if self.toggleButton.isSelected {
//transform button
self.toggleButton.transform = CGAffineTransform(rotationAngle: CGFloat(Double.pi))
//change your ParentView height to desired one
self.constContentViewHeight.constant = self.view.frame.size.height - 220
self.view.layoutIfNeeded()
} else {
self.toggleButton.transform = CGAffineTransform(rotationAngle: CGFloat(Double.pi*2))
// Set height constraint to original value
self.constContentViewHeight.constant = 250
self.view.layoutIfNeeded()
}
}
}
I have created a demo, check it out.
The issue you are facing is due to two animation blocks. So I have changed some lines and put both button transformation and height animation into one animation block.
func openDetail(_ sender: ExpandCourseDetail) {
let isOpen = sender.getOpen() ? CGAffineTransform.identity : CGAffineTransform(rotationAngle: CGFloat.pi)
UIView.animate(withDuration: 1.0, delay: 0.5, options: [.curveEaseInOut], animations: {
sender.transform = rotation
self.contentView.frame.size.height = self.contentView.frame.height + (isOpen ? 300 : -300)
}, completion: { (success) in
sender.setOpen(!isOpen)
})
}

Resources