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 3):
class C { // your view class or whatever
private var displayLink: CADisplayLink?
private var startTime = 0.0
private let animLength = 5.0
func startDisplayLink() {
stopDisplayLink() // make sure to stop a previous running display link
startTime = CACurrentMediaTime() // reset start time
// create displayLink & add it to the run-loop
let displayLink = CADisplayLink(
target: self, selector: #selector(displayLinkDidFire)
)
displayLink.add(to: .main, forMode: .commonModes)
self.displayLink = displayLink
}
@objc func displayLinkDidFire(_ displayLink: CADisplayLink) {
var elapsed = CACurrentMediaTime() - startTime
if elapsed > animLength {
stopDisplayLink()
elapsed = animLength // clamp the elapsed time to the anim 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.