在Swift自定义动画中正确处理/清理CADisplayLink?

Fat*_*tie 19 animation cadisplaylink swift

考虑使用这个简单的同步动画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:
    ????!!!!
    }
Run Code Online (Sandbox Code Playgroud)

似乎存在各种问题.

在(A :),即使link不为null,也可能无法从运行循环中删除它.(例如,有人可能已经初始化它link = link:CADisplayLink()- 尝试崩溃.)

其次在(B :)它似乎是一团糟...肯定有一个更好的(和更多的Swift)方式,如果它是零,即使时间刚刚过期怎么办?

最后在(C :)如果你想打破这个目标......我感到沮丧并且不知道什么是最好的.

实际上A:和B:的代码应该是相同的呼叫权限,是一种清理呼叫.

Ham*_*ish 37

这是一个简单的例子,展示了我如何实现CADisplayLink(在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
  }
}
Run Code Online (Sandbox Code Playgroud)

注意事项:

  • 我们在nil这里用来表示显示链接未运行的状态 - 因为没有简单的方法从无效的显示链接获取此信息.
  • 而不是使用的removeFromRunLoop(),我们正在使用invalidate(),如果显示的链接还没有被添加到一个运行循环,不会崩溃.但是,这种情况绝不应该首先出现 - 因为我们总是在创建后立即将显示链接添加到运行循环中.
  • 我们已经displayLink私有化,以防止外部类将其置于意外状态(例如使其无效但不将其设置为nil).
  • 我们有一个stopDisplayLink()方法可以使显示链接无效(如果它是非零)并将其设置为nil- 而不是复制和粘贴此逻辑.
  • 我们不是要设置pausedtrue显示链接无效之前,因为这是多余的.
  • displayLink我们使用可选链接例如displayLink?.invalidate()(invalidate()如果显示链接不是nil 则调用),而不是强制解包后检查非nil.虽然力量展开在你给定的情况下可能是"安全的"(因为你正在检查为零) - 在未来的重构中它可能是不安全的,因为你可能会重新构造你的逻辑而不考虑它对力量展开的影响.
  • 我们将elapsed时间限制在动画持续时间内,以确保后面的动画逻辑不会产生超出预期范围的值.
  • 我们的update方法根据文档的要求采用displayLinkDidFire(_:)类型的单个参数.CADisplayLink


dig*_*und 5

我意识到这个问题已经有一个很好的答案,但这里有另一种稍微不同的方法,它有助于实现独立于显示链接帧速率的平滑动画。

**(此答案底部提供的演示项目链接 - 更新:演示项目源代码现已更新为 Swift 4)

对于我的实现,我选择将显示链接包装在它自己的类中并设置一个委托引用,该引用将使用增量时间(上次显示链接调用和当前调用之间的时间)进行调用,因此我们可以执行更多动画顺利。

我目前正在使用这种方法在游戏中同时为屏幕周围的约 60 个视图设置动画。

首先,我们将定义我们的包装器将调用以通知更新事件的委托协议。

// defines an interface for receiving display update notifications
protocol DisplayUpdateReceiver: class {
    func displayWillUpdate(deltaTime: CFTimeInterval)
}
Run Code Online (Sandbox Code Playgroud)

接下来我们将定义我们的显示链接包装类。此类将在初始化时采用委托引用。初始化后,它会自动启动我们的显示链接,并在 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)
    }
}
Run Code Online (Sandbox Code Playgroud)

要使用它,您只需初始化包装器的一个实例,传入委托侦听器引用,然后根据增量时间更新动画。在此示例中,委托将更新调用传递给可动画视图(这样您可以跟踪多个动画视图并通过此调用更新每个视图的位置)。

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!)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我们的可移动视图更新功能如下所示:

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
}
Run Code Online (Sandbox Code Playgroud)

如果您想查看完整的演示项目,可以从此处从 GitHub 下载:CADisplayLink 演示项目