I'm animating a clock hand that takes a CGFloat value from 0 to 1. While I have the animation, I would like it to be a lot smoother. The total animation takes 5 seconds, as part of an input variable. How can I make this a lot smoother?
Ideally, I'd like to get all the values from 0 to 1 in 5 seconds...
The clock hand does a complete 360 but is a little choppy
#IBAction func start(_ sender: Any) {
timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(launchTimer), userInfo: nil, repeats: true)
launchTimer()
}
func launchTimer() {
guard seconds < 4.9 else {
timer.invalidate()
seconds = 0
return
}
seconds += 0.1
clockView.currentPressure = CGFloat(seconds / 5)
clockView.setNeedsDisplay()
}
EDIT
import UIKit
class GaugeView: UIView {
var currentPressure : CGFloat = 0.0
override func draw(_ rect: CGRect) {
StyleKitName.drawGauge(pressure: currentPressure)
}
}
Timer is not appropriate for animations on this scale. 100ms isn't a good step in any case, since it's not a multiple of the frame rate (16.67ms). Generally speaking, you shouldn't try to hand-animate unless you have a specialized problem. See UIView.animate(withDuration:...), which is generally how you should animate UI elements, allowing the system to take care of the progress for you.
For a slightly more manual animation, see CABasicAnimation, which will update a property over time. If you need very manual control, see CADisplayLink, but you almost never need this.
In any case, you must never assume that any timer is called precisely when you ask it to be. You cannot add 0.1s to a value just because you asked to be called in 0.1s. You have to look at what time it really is. Even hard-real-time systems can't promise something will be called at a precise moment. The best you can possibly get is a promise it will be within some tolerance (and iOS doesn't even give you that).
To animate this with UIView (which I recommend), it'll probably be something like:
#IBAction func start(_ sender: Any) {
self.clockView.currentPressure = 0
UIView.animate(withDuration: 5, animations: {
self.clockView.currentPressure = 1
})
}
With a CABasicAnimation (which is more complicated) it would be something like:
currentPressure = 1 // You have to set it to where it's going or it'll snap back.
let anim = CABasicAnimation(keyPath: "currentPressure")
anim.fromValue = 0
anim.toValue = 1
anim.duration = 5
clockView.addAnimation(anim)
Make the time interval smaller to make the animation smoother. That way it will seem like it's gliding around instead of jumping between values.
You can also use spritekit:
import SpriteKit
let wait = SKAction.wait(forDuration: 0.01)
let runAnim = SKAction.run {
launchTimer()
}
let n = SKNode()
n.run(SKAction.repeat(SKAction.sequence([wait, runAnim]), count: 500))
Related
#objc private func updateCountdownLabel(_ notification: NSNotification) {
self.progressWidthAnchor.constant = 0
if let timeRemaining = notification.userInfo?["timeRemaining"] as? Int {
self.secondsRemaining = timeRemaining
self.animateProgress(width: 100, duration: 10)
}
}
private func animateProgress(width: CGFloat, duration: Int) {
self.layoutIfNeeded()
UIView.animate(withDuration: TimeInterval(duration),
delay: TimeInterval(LocalConstants.animationDelayDuration),
options: .curveLinear) {
self.progressWidthAnchor.constant = width
self.layoutIfNeeded()
}
}
I'm having a weird issue with my animation. At the moment I am trying to animate a bar decreasing/increasing but I need it to repeat every second.
For some reason it doesn't repeat the animation, the constraint jumps outside the view.
It works the first time, but the second time it doesn't work as you'd expect.
No constraint warnings are thrown.
Moving code around,
updating the layoutIfNeeded to various locations
The cause of my animation not working correctly was due to
removeAllAnimations() not being called prior to the next UIView.animate code blo
Figured it out
Calling removeAllAnimations before executing the code again fixed my issue
I'm trying to learn Core Graphics and am having trouble understanding the behavior of the code I've written, which uses a subclassed UIView and an override of the draw(_ rect:) function.
I've written a basic bouncing ball demo. Any number of random balls are created with random position and speed. They then bounce around the screen.
My issue is the way that the balls appear to move is unexpected and there is a lot of flicker. Here is the sequence inside for loops to iterate through all balls:
Check for collisions.
If there is a collision with the wall, multiply speed by -1.
Increment ball position by ball speed.
I'm currently not clearing the context, so I would expect the existing balls to stay put. Instead they seem to slide smoothly along with the ball that's moving.
I'd like to understand why this is the case.
Here is an image of how the current code runs at 4 fps so that you can see how the shapes are being drawn and shift back and forth:
Here is my code:
class ViewController: UIViewController {
let myView = MyView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBlue
myView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(myView)
NSLayoutConstraint.activate([
myView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
myView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
myView.widthAnchor.constraint(equalTo: view.widthAnchor),
myView.heightAnchor.constraint(equalTo: view.heightAnchor)
])
createDisplayLink(fps: 60)
}
func createDisplayLink(fps: Int) {
let displaylink = CADisplayLink(target: self,
selector: #selector(step))
displaylink.preferredFramesPerSecond = fps
displaylink.add(to: .current,
forMode: RunLoop.Mode.default)
}
#objc func step(displaylink: CADisplayLink) {
myView.setNeedsDisplay()
}
}
class MyView: UIView {
let numBalls = 5
var balls = [Ball]()
override init(frame:CGRect) {
super.init(frame:frame)
for _ in 0..<numBalls {
balls.append(
Ball(
ballPosition: Vec2(x: CGFloat.random(in: 0...UIScreen.main.bounds.width), y: CGFloat.random(in: 0...UIScreen.main.bounds.height)),
ballSpeed: Vec2(x: CGFloat.random(in: 0.5...2), y: CGFloat.random(in: 0.5...2))))
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
for i in 0..<numBalls {
if balls[i].ballPosition.x > self.bounds.width - balls[i].ballSize || balls[i].ballPosition.x < 0 {
balls[i].ballSpeed.x *= -1
}
balls[i].ballPosition.x += balls[i].ballSpeed.x
if balls[i].ballPosition.y > self.bounds.height - balls[i].ballSize || balls[i].ballPosition.y < 0 {
balls[i].ballSpeed.y *= -1
}
balls[i].ballPosition.y += balls[i].ballSpeed.y
}
for i in 0..<numBalls {
context.setFillColor(UIColor.red.cgColor)
context.setStrokeColor(UIColor.green.cgColor)
context.setLineWidth(0)
let rectangle = CGRect(x: balls[i].ballPosition.x, y: balls[i].ballPosition.y, width: balls[i].ballSize, height: balls[i].ballSize)
context.addEllipse(in: rectangle)
context.drawPath(using: .fillStroke)
}
}
}
There are a lot of misunderstandings here, so I'll try to take them one by one:
CADisplayLink does not promise it will call your step method every 1/60 of a second. There's a reason the property is called preferred frames per second. It's just a hint to the system of what you'd like. It may call you less often, and in any case there will be some amount of error.
To perform your own animations by hand, you need to look at what time is actually attached to the given frame, and use that to determine where things are. The CADisplayLink includes a timestamp to let you know that. You can't just increment by speed. You need to multiply speed by actual time to determine distance.
"I'm currently not clearing the context, so I would expect the existing balls to stay put." Every time draw(rect:) is called, you receive a fresh context. It is your responsibility to draw everything for the current frame. There is no persistence between frames. (Core Animation generally provides those kinds of features by efficiently composing CALayers together; but you've chosen to use Core Graphics, and there you need to draw everything every time. We generally do not use Core Graphics this way.)
myView.setNeedsDisplay() does not mean "draw this frame right now." It means "the next time you're going to draw, this view needs to be redrawn." Depending on exactly when the CADisplayLink fires, you may drop a frame, or you might not. Using Core Graphics, you would need to update all the circle's locations before calling setNeedsDisplay(). Then draw(rect:) should just draw them, not compute what they are. (CADisplayLink is really designed to work with CALayers, though, and NSView drawing isn't designed to be updated so often, so this still may be a little tricky to keep smooth.)
The more normal way to create this system would be to generate a CAShapeLayer for each ball and position them on the NSView's layer. Then in the CADisplayLink callback, you would adjust their positions based on the timestamp of the next frame. Alternately, you could just set up a repeating NSTimer or DispatchTimerSource (rather than a CADisplayLink) at something well below the screen refresh speed (like 1/20 s) and move the layer positions in that callback. This would be nice and simple and avoid the complexities of CADisplayLink (which is much more powerful, but expects you to use the timestamp and consider other soft real-time concerns).
Let me preface this question by saying I know it has been asked a few times, but the most upvoted post here was from back in 2014 and the top answer doesn't seem to work anymore.
Issue
In my game, I have all my 'moving' nodes as children of pauseNode. When the pauses button is pressed or when the player leaves the app, pauseScene() is called:
func pauseScene() {
self.node.speed = 0
self.physicsWorld.speed = 0
self.node.isPaused = true
}
This pauses all sprite nodes and emitter nodes. This keeps the sprite nodes paused if the app transitions from background to foreground state, but the particle emitters seem to resume animating.
Current solution
I solved this issue by triggering pauseScene() after a small delay when the app became active.
override func didMove(to view: SKView) {
NotificationCenter.default.addObserver(self, selector: #selector(GameScene.applicationDidBecomeActive(notification:)), name: NSNotification.Name.UIApplicationDidBecomeActive, object: app)
}
#objc func applicationDidBecomeActive(notification: NSNotification) {
NSDelay(0.01) {
pauseScene()
}
}
func NSDelay(_ delay:Double, closure:#escaping ()->()) {
let when = DispatchTime.now() + delay
DispatchQueue.main.asyncAfter(deadline: when, execute: closure)
}
Goal
While this solution pauses the particles, it is not perfect because they are visible moving for the split second it takes to pause them again (this is the shortest delay that actually re-pauses them).
So, after reading the post I linked above, I tried to override applicationDidBecomeActive() in an SKView subclass as was suggested, but I couldn't get the method to be called. I want to prevent these particles from being unpaused in the first place, if possible. Thanks
So, it's been a little over a day and I've found a better solution. A comment in this post suggested having a variable that recorded the status of the scene, and updating it along with the pause/play functions:
var sceneIsPaused = false
func pauseScene() {
self.node.speed = 0
self.physicsWorld.speed = 0
self.node.isPaused = true
sceneIsPaused = true
}
func playScene() {
self.node.speed = 1
self.physicsWorld.speed = 1
self.node.isPaused = false
sceneIsPaused = false
}
Then, override SKView's update method and check the state of that variable. If the scene should be paused, pause it. This will mean that if the scene is automatically unpaused it will re-pause the next frame. This is much faster and cleaner than setting a delay:
override func update(_ currentTime: TimeInterval) {
if (sceneIsPaused == true) {
self.node.speed = 0
self.physicsWorld.speed = 0
self.node.isPaused = true
}
}
I have ViewController with stars rating that looks like this (except that there are 10 stars)
When user opens ViewController for some object that have no rating I want to point user's attention to this stars with very simple way: animate stars highlighting (you could see such behaviour on some ads in real world when each letter is highlighted one after another).
One star highlighted
Two stars highlighted
Three stars highlighted
......
Turn off all of them
So this is the way how I am doing it
func delayWithSeconds(_ seconds: Double, completion: #escaping () -> ()) {
DispatchQueue.main.asyncAfter(deadline: .now() + seconds) {
completion()
}
}
func ratingStarsAnimation() {
for i in 1...11 {
var timer : Double = 0.6 + Double(i)*0.12
delayWithSeconds(timer) {
ratingStars.rating = (i < 10) ? Double(i) : 0
}
}
}
What is going on here? I have function called delayWithSeconds that delays action and I use this function to delay each star highlighting. And 0.6 is initial delay before animation begins. After all stars are highlighted - last step is to turn off highlighting of all stars.
This code works but I can't say that it is smooth.
My questions are:
How can I change 0.6 + Double(i)*0.12 to get smooth animation feel?
I think that my solution with delays is not good - how can I solve smooth stars highlighting task better?
Have a look at the CADisplaylink class. Its a specialized timer that is linked to the refresh rate of the screen, on iOS this is 60fps.
It's the backbone of many 3rd party animation libraries.
Usage example:
var displayLink: CADisplayLink?
let start: Double = 0
let end: Double = 10
let duration: CFTimeInterval = 5 // seconds
var startTime: CFTimeInterval = 0
let ratingStars = RatingView()
func create() {
displayLink = CADisplayLink(target: self, selector: #selector(tick))
displayLink?.add(to: .main, forMode: .defaultRunLoopMode)
}
func tick() {
guard let link = displayLink else {
cleanup()
return
}
if startTime == 0 { // first tick
startTime = link.timestamp
return
}
let maxTime = startTime + duration
let currentTime = link.timestamp
guard currentTime < maxTime else {
finish()
return
}
// Add math here to ease the animation
let progress = (currentTime - startTime) / duration
let progressInterval = (end - start) * Double(progress)
// get value =~ 0...10
let normalizedProgress = start + progressInterval
ratingStars.rating = normalizedProgress
}
func finish() {
ratingStars.rating = 0
cleanup()
}
func cleanup() {
displayLink?.remove(from: .main, forMode: .defaultRunLoopMode)
displayLink = nil
startTime = 0
}
As a start this will allow your animation to be smoother. You will still need to add some trigonometry if you want to add easing but that shouldn't be too difficult.
CADisplaylink:
https://developer.apple.com/reference/quartzcore/cadisplaylink
Easing curves: http://gizma.com/easing/
I am trying to repeat a set of actions by running them through a loop. My loop is fine when running something else, but it seems to have trouble running the actions. My code is as follows:
let pulse1 = SKAction.scaleTo(2.0, duration: 1.0)
let pulse2 = SKAction.scaleTo(0.5, duration: 1.0)
var i = 0
override func didMoveToView(view: SKView) {
for var i = 0; i <= 100; i++ {
self.sun.runAction(pulse1)
self.sun.runAction(pulse2)
}
This will cause the node to pulse1 and pulse2 each once but never again. If I add
println("")
to the loop, it runs whatever text properly, but for some reason doesn't run the actions like it runs the text. Or maybe it does and I don't understand how SKAction works? Either way, the loop is executing properly, I believe. I am not quite sure what's wrong with the SKAction call in the loop.
scaleTo simply changes the node's scale. Once pulse1 goes to 2.0 and pulse2 gets to 0.5, runAction runs repeatedly but you never change the scale for either pulse ever again.
That's why you're only seeing it do work the first time.
Instead of using a for loop, try something like this:
override func didMoveToView(view: SKView) {
if (i % 2 == 0) {
let pulse = SKAction.scaleTo(2.0, duration: 1.0)
} else {
let pulse = SKAction.scaleTo(0.5, duration: 1.0)
}
[self.sun runAction:pulse completion:^{
if( i < 100 )
{
didMoveToView(view);
}
}];
}
Maybe you can use
class func repeatAction(_ action: SKAction,
count count: Int) -> SKAction
Put as many single actions in a sequence an run repeatAction for x times.