Consider this trivial sync animation using CADisplayLink
,
var link:CADisplayLink?
var startTime:Double = 0.0
let animTime:Double = 0.2
let animMaxVal:CGFloat = 0.4
private func yourAnim()
{
if ( link != nil )
{
link!.paused = true
//A:
link!.removeFromRunLoop(
NSRunLoop.mainRunLoop(), forMode:NSDefaultRunLoopMode)
link = nil
}
link = CADisplayLink(target: self, selector: #selector(doorStep) )
startTime = CACurrentMediaTime()
link!.addToRunLoop(
NSRunLoop.currentRunLoop(), forMode:NSDefaultRunLoopMode)
}
func doorStep()
{
let elapsed = CACurrentMediaTime() - startTime
var ping = elapsed
if (elapsed > (animTime / 2.0)) {ping = animTime - elapsed}
let frac = ping / (animTime / 2.0)
yourAnimFunction(CGFloat(frac) * animMaxVal)
if (elapsed > animTime)
{
//B:
link!.paused = true
link!.removeFromRunLoop(
NSRunLoop.mainRunLoop(), forMode:NSDefaultRunLoopMode)
link = nil
yourAnimFunction(0.0)
}
}
func killAnimation()
{
// for example if the cell disappears or is reused
//C:
????!!!!
}
There seems to be various problems.
At (A:), even though link
is not null, it may not be possible to remove it from a run loop. (For example, someone may have initialized it with link = link:CADisplayLink()
- try it for a crash.)
Secondly at (B:) it seems to be a mess ... surely there's a better (and more Swift) way, and what if it's nil even though the time just expired?
Finally in (C:) if you want to break the anim ... I got depressed and have no clue what is best.
And really the code at A: and B: should be the same call right, kind of a clean-up call.
Here’s a simple example showing how I’d go about implementing a CADisplayLink
(in Swift 5):
class C { /// your view class or whatever
private var displayLink: CADisplayLink?
private var startTime = 0.0
private let animationLength = 5.0
func startDisplayLink() {
stopDisplayLink() /// make sure to stop a previous running display link
startTime = CACurrentMediaTime() // reset start time
/// create displayLink and add it to the run-loop
let displayLink = CADisplayLink(target: self, selector: #selector(displayLinkDidFire))
displayLink.add(to: .main, forMode: .common)
self.displayLink = displayLink
}
@objc func displayLinkDidFire(_ displayLink: CADisplayLink) {
var elapsedTime = CACurrentMediaTime() - startTime
if elapsedTime > animationLength {
stopDisplayLink()
elapsedTime = animationLength /// clamp the elapsed time to the animation length
}
/// do your animation logic here
}
/// invalidate display link if it's non-nil, then set to nil
func stopDisplayLink() {
displayLink?.invalidate()
displayLink = nil
}
}
Points to note:
nil
here to represent the state in which the display link isn’t running – as there’s no easy way of getting this information from an invalidated display link.removeFromRunLoop()
, we’re using invalidate()
, which will not crash if the display link hasn’t already been added to a run-loop. However this situation should never arise in the first place – as we’re always immediately adding the display link to the run-loop after creating it.displayLink
private in order to prevent outside classes from putting it in an unexpected state (e.g invalidating it but not setting it to nil
).stopDisplayLink()
method that both invalidates the display link (if it is non-nil) and sets it to nil
– rather than copy and pasting this logic.paused
to true
before invalidating the display link, as this is redundant.displayLink
after checking for non-nil, we’re using optional chaining e.g displayLink?.invalidate()
(which will call invalidate()
if the display link isn’t nil). While force unwrapping may be ‘safe’ in your given situation (as you’re checking for nil) – it’s potentially unsafe when it comes to future refactoring, as you may re-structure your logic without considering what impact this has on the force unwraps.elapsed
time to the animation duration in order to ensure that the later animation logic doesn’t produce a value out of the expected range.displayLinkDidFire(_:)
takes a single argument of type CADisplayLink
, as required by the documentation.I realize this question already has a good answer, but here's another slightly different approach that helps in implementing smooth animations independent of the display link frame rate.
**(Link to demo project available at the bottom of this answer - UPDATE: demo project source code now updated to Swift 4)
For my implementation I opted to wrap the display link in it's own class and setup a delegate reference that will get called with the delta time (the time between the last display link call and the current call) so we can perform our animations a little more smoothly.
I'm currently using this method to animate ~60 views around the screen simultaneously in a game.
First we're going to define the delegate protocol that our wrapper will call to notify of update events.
// defines an interface for receiving display update notifications
protocol DisplayUpdateReceiver: class {
func displayWillUpdate(deltaTime: CFTimeInterval)
}
Next we're going to define our display link wrapper class. This class will take a delegate reference on initialization. When initialized it will automatically start our display link, and clean it up on deinit.
import UIKit
class DisplayUpdateNotifier {
// **********************************************
// MARK: Variables
// **********************************************
/// A weak reference to the delegate/listener that will be notified/called on display updates
weak var listener: DisplayUpdateReceiver?
/// The display link that will be initiating our updates
internal var displayLink: CADisplayLink? = nil
/// Tracks the timestamp from the previous displayLink call
internal var lastTime: CFTimeInterval = 0.0
// **********************************************
// MARK: Setup & Tear Down
// **********************************************
deinit {
stopDisplayLink()
}
init(listener: DisplayUpdateReceiver) {
// setup our delegate listener reference
self.listener = listener
// setup & kick off the display link
startDisplayLink()
}
// **********************************************
// MARK: CADisplay Link
// **********************************************
/// Creates a new display link if one is not already running
private func startDisplayLink() {
guard displayLink == nil else {
return
}
displayLink = CADisplayLink(target: self, selector: #selector(linkUpdate))
displayLink?.add(to: .main, forMode: .commonModes)
lastTime = 0.0
}
/// Invalidates and destroys the current display link. Resets timestamp var to zero
private func stopDisplayLink() {
displayLink?.invalidate()
displayLink = nil
lastTime = 0.0
}
/// Notifier function called by display link. Calculates the delta time and passes it in the delegate call.
@objc private func linkUpdate() {
// bail if our display link is no longer valid
guard let displayLink = displayLink else {
return
}
// get the current time
let currentTime = displayLink.timestamp
// calculate delta (
let delta: CFTimeInterval = currentTime - lastTime
// store as previous
lastTime = currentTime
// call delegate
listener?.displayWillUpdate(deltaTime: delta)
}
}
To use it you simply initialize an instance of the wrapper, passing in the delegate listener reference, then update your animations based on the delta time. In this example, the delegate passes the update call off to the animatable view (this way you could track multiple animating views and have each update their positions via this call).
class ViewController: UIViewController, DisplayUpdateReceiver {
var displayLinker: DisplayUpdateNotifier?
var animView: MoveableView?
override func viewDidLoad() {
super.viewDidLoad()
// setup our animatable view and add as subview
animView = MoveableView.init(frame: CGRect.init(x: 150.0, y: 400.0, width: 20.0, height: 20.0))
animView?.configureMovement()
animView?.backgroundColor = .blue
view.addSubview(animView!)
// setup our display link notifier wrapper class
displayLinker = DisplayUpdateNotifier.init(listener: self)
}
// implement DisplayUpdateReceiver function to receive updates from display link wrapper class
func displayWillUpdate(deltaTime: CFTimeInterval) {
// pass the update call off to our animating view or views
_ = animView?.update(deltaTime: deltaTime)
// in this example, the animatable view will remove itself from its superview when its animation is complete and set a flag
// that it's ready to be used. We simply check if it's ready to be recycled, if so we reset its position and add it to
// our view again
if animView?.isReadyForReuse == true {
animView?.reset(center: CGPoint.init(x: CGFloat.random(low: 20.0, high: 300.0), y: CGFloat.random(low: 20.0, high: 700.0)))
view.addSubview(animView!)
}
}
}
Our moveable views update function looks like this:
func update(deltaTime: CFTimeInterval) -> Bool {
guard canAnimate == true, isReadyForReuse == false else {
return false
}
// by multiplying our x/y values by the delta time new values are generated that will generate a smooth animation independent of the framerate.
let smoothVel = CGPoint(x: CGFloat(Double(velocity.x)*deltaTime), y: CGFloat(Double(velocity.y)*deltaTime))
let smoothAccel = CGPoint(x: CGFloat(Double(acceleration.x)*deltaTime), y: CGFloat(Double(acceleration.y)*deltaTime))
// update velocity with smoothed acceleration
velocity.adding(point: smoothAccel)
// update center with smoothed velocity
center.adding(point: smoothVel)
currentTime += 0.01
if currentTime >= timeLimit {
canAnimate = false
endAnimation()
return false
}
return true
}
If you'd like to look through a full demo project you can download it from GitHub here: CADisplayLink Demo Project
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With